No heredarás de std :: vector

189

Ok, esto es realmente difícil de confesar, pero tengo una fuerte tentación en este momento para heredar std::vector.

Necesito unos 10 algoritmos personalizados para el vector y quiero que sean directamente miembros del vector. Pero, naturalmente, también quiero tener el resto de std::vectorla interfaz. Bueno, mi primera idea, como ciudadano respetuoso de la ley, era tener un std::vectormiembro en MyVectorclase. Pero luego tendría que volver a proporcionar manualmente toda la interfaz std :: vector. Demasiado para escribir. Luego, pensé en la herencia privada, de modo que en lugar de proporcionar métodos, escribiría un montón de using std::vector::member's en la sección pública. Esto es tedioso también en realidad.

Y aquí estoy, realmente creo que simplemente puedo heredar públicamente std::vector, pero dar una advertencia en la documentación de que esta clase no debe usarse polimórficamente. Creo que la mayoría de los desarrolladores son lo suficientemente competentes como para entender que esto no debería usarse polimórficamente de todos modos.

¿Es mi decisión absolutamente injustificable? Si es así, ¿por qué? ¿Puede proporcionar una alternativa que tendría los miembros adicionales en realidad miembros pero que no implicaría volver a escribir toda la interfaz del vector? Lo dudo, pero si puedes, seré feliz.

Además, aparte del hecho de que algún idiota puede escribir algo como

std::vector<int>* p  = new MyVector

¿Hay algún otro peligro realista al usar MyVector? Al decir realista, descarto cosas como imaginar una función que toma un puntero para vectorizar ...

Bueno, he declarado mi caso. He pecado. Ahora depende de ti perdonarme o no :)

Armen Tsirunyan
fuente
9
Entonces, ¿básicamente pregunta si está bien violar una regla común basada en el hecho de que es demasiado flojo para volver a implementar la interfaz del contenedor? Entonces no, no lo es. Mira, puedes tener lo mejor de ambos mundos si te tragas esa píldora amarga y lo haces correctamente. No seas ese tipo. Escribe código robusto.
Jim Brissom
77
¿Por qué no puede / no desea agregar la funcionalidad que necesita con funciones que no son miembros? Para mí, eso sería lo más seguro en este escenario.
Simone
11
std::vectorLa interfaz de @Jim: es bastante grande, y cuando aparezca C ++ 1x, se expandirá considerablemente. Eso es mucho para escribir y más para expandirse en unos pocos años. Creo que esta es una buena razón para considerar la herencia en lugar de la contención, si se sigue la premisa de que esas funciones deberían ser miembros (lo cual dudo). La regla para no derivar de los contenedores STL es que no son polimórficos. Si no los está utilizando de esa manera, no se aplica.
sbi
9
El verdadero meollo de la pregunta está en la frase: "Quiero que sean miembros directos del vector". Nada más en la pregunta realmente importa. ¿Por qué quieres esto? ¿Cuál es el problema con solo proporcionar esta funcionalidad como no miembros?
jalf
8
@JoshC: "Deberás" siempre ha sido más común que "deberías", y también es la versión que se encuentra en la Biblia King James (que generalmente es a lo que la gente alude cuando escribe "no [...] "). ¿Qué demonios te llevaría a llamarlo "error ortográfico"?
ruakh

Respuestas:

155

En realidad, no hay nada malo con la herencia pública de std::vector. Si necesitas esto, solo hazlo.

Sugeriría hacerlo solo si es realmente necesario. Solo si no puede hacer lo que quiere con funciones gratuitas (por ejemplo, debe mantener algún estado).

El problema es que MyVectores una entidad nueva. Significa que un nuevo desarrollador de C ++ debería saber qué demonios es antes de usarlo. ¿Cuál es la diferencia entre std::vectory MyVector? ¿Cuál es mejor usar aquí y allá? ¿Qué pasa si necesito mover std::vectora MyVector? ¿Puedo usar swap()o no?

No produzca nuevas entidades solo para hacer que algo se vea mejor. Estas entidades (especialmente, tan comunes) no van a vivir en el vacío. Vivirán en ambientes mixtos con entropía constantemente incrementada.

Stas
fuente
77
Mi único argumento en contra de esto es que uno realmente debe saber lo que está haciendo para hacer esto. Por ejemplo, no introduzca miembros de datos adicionales MyVectory luego intente pasarlos a funciones que acepten std::vector&o std::vector*. Si hay algún tipo de asignación de copia involucrada usando std :: vector * o std :: vector &, tenemos problemas de corte donde los nuevos miembros de datos MyVectorno se copiarán. Lo mismo sería cierto para llamar a swap a través de un puntero / referencia base. Tiendo a pensar que cualquier tipo de jerarquía de herencia que corre el riesgo de segmentar objetos es mala.
stinky472
13
std::vectorEl destructor no lo es virtual, por lo tanto, nunca debes heredar de él
André Fratelli
2
Creé una clase que heredó públicamente std :: vector por este motivo: tenía un código antiguo con una clase de vector que no era STL y quería pasar a STL. Reimplementé la clase anterior como una clase derivada de std :: vector, lo que me permitió continuar usando los nombres de funciones anteriores (por ejemplo, Count () en lugar de size ()) en el código antiguo, mientras escribía código nuevo usando std :: vector funciones No agregué ningún miembro de datos, por lo tanto, el destructor de std :: vector funcionó bien para los objetos creados en el montón.
Graham Asher
3
@GrahamAsher Si elimina un objeto a través de un puntero a la base, y el destructor no es virtual, su programa exhibe un comportamiento indefinido. Un posible resultado del comportamiento indefinido es "funcionó bien en mis pruebas". Otra es que envía un correo electrónico a tu abuela con tu historial de navegación web. Ambos cumplen con el estándar C ++. El cambio de uno a otro con versiones puntuales de compiladores, sistemas operativos o la fase de la luna también es compatible.
Yakk - Adam Nevraumont
2
@GrahamAsher No, cada vez que elimina cualquier objeto a través de un puntero a la base sin un destructor virtual, ese es un comportamiento indefinido según el estándar. Entiendo lo que crees que está sucediendo; Simplemente te equivocas. "el destructor de la clase base se llama y funciona" es un síntoma posible (y el más común) de este comportamiento indefinido, porque ese es el código de máquina ingenuo que generalmente genera el compilador. Esto no lo hace seguro ni una gran idea para hacer.
Yakk - Adam Nevraumont
92

Todo el STL fue diseñado de tal manera que los algoritmos y los contenedores están separados .

Esto condujo a un concepto de diferentes tipos de iteradores: iteradores constantes, iteradores de acceso aleatorio, etc.

Por lo tanto, le recomiendo que acepte esta convención y diseñe sus algoritmos de tal manera que no les importe en qué contenedor están trabajando , y solo requerirían un tipo específico de iterador que necesitarían para realizar su operaciones

Además, déjame redirigirte a algunos buenos comentarios de Jeff Attwood .

Kos
fuente
63

La razón principal para no heredar std::vectorpúblicamente es la ausencia de un destructor virtual que efectivamente evite el uso polimórfico de descendientes. En particular, no se le permite a deleteun std::vector<T>*que realmente apunta a un objeto derivado (incluso si la clase derivada no agrega miembros), sin embargo, el compilador generalmente no puede advertirle al respecto.

La herencia privada está permitida bajo estas condiciones. Por lo tanto, recomiendo usar la herencia privada y reenviar los métodos requeridos del padre como se muestra a continuación.

class AdVector: private std::vector<double>
{
    typedef double T;
    typedef std::vector<double> vector;
public:
    using vector::push_back;
    using vector::operator[];
    using vector::begin;
    using vector::end;
    AdVector operator*(const AdVector & ) const;
    AdVector operator+(const AdVector & ) const;
    AdVector();
    virtual ~AdVector();
};

Primero debe considerar refactorizar sus algoritmos para abstraer el tipo de contenedor en el que están operando y dejarlos como funciones con plantilla libre, como lo señala la mayoría de los que responden. Esto generalmente se hace haciendo que un algoritmo acepte un par de iteradores en lugar de contenedor como argumentos.

Basilevs
fuente
IIUC, la ausencia de un destructor virtual es solo un problema si la clase derivada asigna recursos que deben liberarse tras la destrucción. (No se liberarían en un caso de uso polimórfico porque un contexto que, sin saberlo, toma posesión de un objeto derivado a través del puntero a la base solo llamaría al destructor de la base cuando sea el momento). Problemas similares surgen de otras funciones de miembro anuladas, por lo que debe tener cuidado se debe tener en cuenta que las bases son válidas para llamar. Pero en ausencia de recursos adicionales, ¿hay alguna otra razón?
Peter - Restablece a Monica
2
vectorEl almacenamiento asignado no es el problema: después de todo, vectorel destructor se llamaría directamente a través de un puntero a vector. Es solo que el estándar prohíbe deletelos objetos de la tienda libre a través de una expresión de clase base. Seguramente, la razón es que el mecanismo de (des) asignación puede tratar de inferir el tamaño del fragmento de memoria para liberarlo del deleteoperando, por ejemplo, cuando hay varias arenas de asignación para objetos de ciertos tamaños. Esta restricción, afaics, no se aplica a la destrucción normal de objetos con una duración de almacenamiento estático o automático.
Peter - Restablece a Mónica el
@DavisHerring Creo que estamos de acuerdo allí :-).
Peter - Restablece a Mónica
@DavisHerring Ah, ya veo, te refieres a mi primer comentario: había un IIUC en ese comentario, y terminó en una pregunta; Más tarde vi que, de hecho, siempre está prohibido. (Basilevs había hecho una declaración general, "efectivamente previene", y me pregunté sobre la forma específica en que previene.) Entonces sí, estamos de acuerdo: UB.
Peter - Restablece a Mónica
@Basilevs Eso debe haber sido involuntario. Fijo.
ThomasMcLeod
36

Si está considerando esto, claramente ya ha matado a los pedantes de idiomas en su oficina. Con ellos fuera del camino, ¿por qué no simplemente hacer

struct MyVector
{
   std::vector<Thingy> v;  // public!
   void func1( ... ) ; // and so on
}

Eso evitará todos los errores posibles que puedan surgir de la transmisión accidental de su clase MyVector, y aún puede acceder a todas las operaciones de vectores simplemente agregando un poco .v.

Crashworks
fuente
¿Y exponiendo contenedores y algoritmos? Ver la respuesta de Kos arriba.
bruno nery
19

¿Qué esperas lograr? ¿Solo proporciona alguna funcionalidad?

La forma idiomática de C ++ de hacer esto es simplemente escribir algunas funciones gratuitas que implementan la funcionalidad. Lo más probable es que realmente no necesite un std :: vector, específicamente para la funcionalidad que está implementando, lo que significa que realmente está perdiendo la reutilización al intentar heredar de std :: vector.

Le recomiendo encarecidamente que mire la biblioteca y los encabezados estándar y que medite sobre cómo funcionan.

Karl Knechtel
fuente
55
No estoy convencido. ¿Podría actualizar con algunos de los códigos propuestos para explicar por qué?
Karl Knechtel
66
@Armen: además de la estética, ¿hay alguna buena razón?
snemarch
12
@Armen: Mejor estética, y mayor genérico, sería proporcionar funciones gratuitas fronty backtambién. :) (Considere también el ejemplo de gratis beginy enden C ++ 0x y boost.)
UncleBens
3
Todavía no sé qué hay de malo con las funciones gratuitas. Si no le gusta la "estética" del STL, tal vez C ++ sea el lugar equivocado para usted, estéticamente. Y agregar algunas funciones miembro no lo solucionará, ya que muchos otros algoritmos siguen siendo funciones libres.
Frank Osterfeld
17
Es difícil almacenar en caché el resultado de una operación pesada en un algoritmo externo. Suponga que tiene que calcular una suma de todos los elementos en el vector o resolver una ecuación polinómica con elementos vectoriales como coeficientes. Esas operaciones son pesadas y la pereza sería útil para ellos. Pero no puede introducirlo sin envolverlo o heredarlo del contenedor.
Basilevs
14

Creo que muy pocas reglas deben seguirse ciegamente el 100% del tiempo. Parece que lo has pensado bastante y estás convencido de que este es el camino a seguir. Entonces, a menos que alguien presente buenas razones específicas para no hacer esto, creo que debe seguir adelante con su plan.

NPE
fuente
9
Tu primera oración es verdadera el 100% del tiempo. :)
Steve Fallows el
55
Lamentablemente, la segunda oración no lo es. No lo ha pensado mucho. La mayor parte de la pregunta es irrelevante. La única parte que muestra su motivación es "Quiero que sean miembros directos del vector". Quiero. No hay razón por la cual esto es deseable. Lo que parece que no lo ha pensado en absoluto .
jalf
7

No hay ninguna razón para heredar a std::vectormenos que uno quiera hacer una clase que funcione de manera diferente a std::vector, porque maneja a su manera los detalles ocultos de std::vectorla definición, o a menos que uno tenga razones ideológicas para usar los objetos de dicha clase en lugar de std::vectorDe los. Sin embargo, los creadores del estándar en C ++ no proporcionaron std::vectorninguna interfaz (en forma de miembros protegidos) que dicha clase heredada podría aprovechar para mejorar el vector de una manera específica. De hecho, no tenían forma de pensar en ningún aspecto específico que pudiera necesitar extensión o afinar la implementación adicional, por lo que no tenían que pensar en proporcionar una interfaz de este tipo para ningún propósito.

Los motivos de la segunda opción pueden ser solo ideológicos, porque los std::vectors no son polimórficos, y de lo contrario no hay diferencia si expone std::vectorla interfaz pública a través de la herencia pública o de la membresía pública. (Suponga que necesita mantener algún estado en su objeto para que no pueda salirse con las funciones libres). En una nota menos sólida y desde el punto de vista ideológico, parece que los std::vectors son una especie de "idea simple", por lo que cualquier complejidad en forma de objetos de diferentes clases posibles en su lugar ideológicamente no sirve de nada.

Evgeniy
fuente
Gran respuesta. Bienvenido a SO!
Armen Tsirunyan
4

En términos prácticos: si no tiene ningún miembro de datos en su clase derivada, no tiene ningún problema, ni siquiera en el uso polimórfico. Solo necesita un destructor virtual si los tamaños de la clase base y la clase derivada son diferentes y / o tiene funciones virtuales (lo que significa una tabla v).

PERO en teoría: desde [expr.delete] en el C ++ 0x FCD: en la primera alternativa (eliminar objeto), si el tipo estático del objeto a eliminar es diferente de su tipo dinámico, el tipo estático será un clase base del tipo dinámico del objeto que se va a eliminar y el tipo estático tendrá un destructor virtual o el comportamiento no está definido.

Pero puede derivar en privado de std :: vector sin problemas. He usado el siguiente patrón:

class PointVector : private std::vector<PointType>
{
    typedef std::vector<PointType> Vector;
    ...
    using Vector::at;
    using Vector::clear;
    using Vector::iterator;
    using Vector::const_iterator;
    using Vector::begin;
    using Vector::end;
    using Vector::cbegin;
    using Vector::cend;
    using Vector::crbegin;
    using Vector::crend;
    using Vector::empty;
    using Vector::size;
    using Vector::reserve;
    using Vector::operator[];
    using Vector::assign;
    using Vector::insert;
    using Vector::erase;
    using Vector::front;
    using Vector::back;
    using Vector::push_back;
    using Vector::pop_back;
    using Vector::resize;
    ...
hmuelner
fuente
3
"Solo necesita un destructor virtual si los tamaños de la clase base y la clase derivada son diferentes nad / o si tiene funciones virtuales (lo que significa una tabla v)". Esta afirmación es prácticamente correcta, pero no teóricamente
Armen Tsirunyan
2
Sí, en principio sigue siendo un comportamiento indefinido.
jalf
Si afirma que este es un comportamiento indefinido, me gustaría ver una prueba (cita del estándar).
hmuelner
8
@hmuelner: Desafortunadamente, Armen y jalf están en lo correcto en este caso. Desde [expr.delete]en el FCD de C ++ 0x: <cita> En la primera alternativa (eliminar objeto), si el tipo estático del objeto a eliminar es diferente de su tipo dinámico, el tipo estático será una clase base del tipo dinámico del objeto que se va a eliminar y el tipo estático tendrá un destructor virtual o el comportamiento no está definido. </quote>
Ben Voigt
1
Lo cual es divertido, porque en realidad pensé que el comportamiento dependía de la presencia de un destructor no trivial (específicamente, que las clases POD podrían destruirse a través de un puntero a base).
Ben Voigt
3

Si sigue un buen estilo de C ++, la ausencia de función virtual no es el problema, sino el corte (consulte https://stackoverflow.com/a/14461532/877329 )

¿Por qué la ausencia de funciones virtuales no es el problema? Porque una función no debe intentar con deleteningún puntero que reciba, ya que no es propietaria de ella. Por lo tanto, si se siguen estrictas políticas de propiedad, los destructores virtuales no deberían ser necesarios. Por ejemplo, esto siempre es incorrecto (con o sin destructor virtual):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    delete obj;
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj); //Will crash here. But caller does not know that
//  ...
    }

Por el contrario, esto siempre funcionará (con o sin destructor virtual):

void foo(SomeType* obj)
    {
    if(obj!=nullptr) //The function prototype only makes sense if parameter is optional
        {
        obj->doStuff();
        }
    }

class SpecialSomeType:public SomeType
    {
    // whatever 
    };

int main()
    {
    SpecialSomeType obj;
    doStuff(&obj);
//  The correct destructor *will* be called here.
    }

Si el objeto es creado por una fábrica, la fábrica también debe devolver un puntero a un eliminador de trabajo, que debe usarse en lugar de delete, ya que la fábrica puede usar su propio montón. La persona que llama puede obtenerlo en forma de a share_ptro unique_ptr. En resumen, no deletecualquier cosa que usted no recibió directamente desde new.

user877329
fuente
2

Sí, es seguro siempre que tenga cuidado de no hacer las cosas que no son seguras ... No creo que haya visto a nadie usar un vector nuevo, por lo que en la práctica probablemente estará bien. Sin embargo, no es el idioma común en c ++ ...

¿Puede dar más información sobre cuáles son los algoritmos?

A veces terminas yendo por un camino con un diseño y luego no puedes ver los otros caminos que podrías haber tomado, el hecho de que afirmas que necesitas vectores con 10 nuevos algoritmos suena campanas de alarma para mí, ¿hay realmente 10 propósitos generales? algoritmos que un vector puede implementar, o ¿está tratando de hacer un objeto que sea a la vez un vector de propósito general Y que contenga funciones específicas de la aplicación?

Ciertamente no estoy diciendo que no debas hacer esto, es solo que con la información que has dado están sonando las alarmas, lo que me hace pensar que tal vez algo está mal con tus abstracciones y que hay una mejor manera de lograr lo que querer.

jcoder
fuente
2

También heredé std::vectorrecientemente y me pareció muy útil y hasta ahora no he tenido ningún problema.

Mi clase es una clase de matriz dispersa, lo que significa que necesito almacenar mis elementos de matriz en algún lugar, es decir, en un std::vector. Mi razón para heredar fue que era un poco flojo para escribir interfaces para todos los métodos y también estoy interconectando la clase a Python a través de SWIG, donde ya hay un buen código de interfaz para std::vector. Me resultó mucho más fácil extender este código de interfaz a mi clase en lugar de escribir uno nuevo desde cero.

El único problema que veo con el enfoque no es tanto con el destructor no virtual, sino que algunos otros métodos, los cuales me gustaría a la sobrecarga, tales como push_back(), resize(),insert() etc. herencia privada de hecho podría ser una buena opción.

¡Gracias!

Joel Andersson
fuente
10
En mi experiencia, la avería más desfavorable a largo plazo es a menudo causada por la gente que intenta algo mal aconsejado, y " hasta el momento no han experimentado (leer notado ) ningún problema con ella".
Desilusionado
0

Aquí, permítanme presentarles 2 formas más de hacer lo que quieren. Una es otra forma de envolver std::vector, otra es la forma de heredar sin dar a los usuarios la oportunidad de romper nada:

  1. Permítanme agregar otra forma de envolver std::vectorsin escribir muchos envoltorios de funciones.

#include <utility> // For std:: forward
struct Derived: protected std::vector<T> {
    // Anything...
    using underlying_t = std::vector<T>;

    auto* get_underlying() noexcept
    {
        return static_cast<underlying_t*>(this);
    }
    auto* get_underlying() const noexcept
    {
        return static_cast<underlying_t*>(this);
    }

    template <class Ret, class ...Args>
    auto apply_to_underlying_class(Ret (*underlying_t::member_f)(Args...), Args &&...args)
    {
        return (get_underlying()->*member_f)(std::forward<Args>(args)...);
    }
};
  1. Heredar de std :: span en lugar de std::vectory evitar el problema dtor.
JiaHao Xu
fuente
0

Se garantiza que esta pregunta producirá un agarre de perlas sin aliento, pero de hecho no hay ninguna razón defendible para evitar, o "multiplicar innecesariamente las entidades" para evitar, la derivación de un contenedor estándar. La expresión más simple y corta posible es la más clara y la mejor.

Es necesario ejercer todo el cuidado habitual en torno a cualquier tipo derivado, pero no hay nada de especial en el caso de una base del Estándar. Anular una función de miembro base podría ser complicado, pero eso no sería prudente hacer con una base no virtual, por lo que no hay mucho especial aquí. Si tuviera que agregar un miembro de datos, debería preocuparse por cortar si el miembro tuviera que mantenerse coherente con el contenido de la base, pero de nuevo, eso es lo mismo para cualquier base.

El lugar donde he encontrado que derivar de un contenedor estándar es particularmente útil es agregar un solo constructor que realice precisamente la inicialización necesaria, sin posibilidad de confusión o secuestro por parte de otros constructores. (¡Te estoy mirando, constructores de initialization_list!) Entonces, puedes usar libremente el objeto resultante, cortado - pasarlo por referencia a algo que espera la base, pasar de él a una instancia de la base, ¿qué tienes? No hay casos extremos de los que preocuparse, a menos que le moleste vincular un argumento de plantilla a la clase derivada.

Un lugar donde esta técnica será inmediatamente útil en C ++ 20 es la reserva. Donde podríamos haber escrito

  std::vector<T> names; names.reserve(1000);

podemos decir

  template<typename C> 
  struct reserve_in : C { 
    reserve_in(std::size_t n) { this->reserve(n); }
  };

y luego, incluso como miembros de la clase,

  . . .
  reserve_in<std::vector<T>> taken_names{1000};  // 1
  std::vector<T> given_names{reserve_in<std::vector<T>>{1000}}; // 2
  . . .

(según preferencia) y no es necesario escribir un constructor solo para llamar a reserve () en ellos.

(La razón por la que reserve_in, técnicamente, debe esperar a C ++ 20 es que los estándares anteriores no requieren que la capacidad de un vector vacío se conserve en los movimientos. Eso se reconoce como un descuido y se puede esperar que se repare razonablemente como un defecto a tiempo para el '20. También podemos esperar que la solución sea, efectivamente, retrocedida a los Estándares anteriores, porque todas las implementaciones existentes realmente conservan la capacidad en los movimientos; los Estándares simplemente no lo han requerido. la reserva de armas casi siempre es solo una optimización de todos modos).

Algunos argumentarían que el caso de reserve_ines mejor atendido por una plantilla de función gratuita:

  template<typename C> 
  auto reserve_in(std::size_t n) { C c; c.reserve(n); return c; }

Tal alternativa es ciertamente viable, y podría incluso, a veces, ser infinitamente más rápida, debido a * RVO. Pero la elección de la derivación o la función libre debe hacerse por sus propios méritos, y no a partir de una superstición sin fundamento (¡je!) Sobre derivar de componentes estándar. En el ejemplo de uso anterior, solo la segunda forma funcionaría con la función libre; aunque fuera del contexto de la clase, podría escribirse de manera más concisa:

  auto given_names{reserve_in<std::vector<T>>(1000)}; // 2
Nathan Myers
fuente