c++ inheritance vector libstdc++

La implementación libstdc++ de gcc4.9.2 de std:: vector hereda de_Vector_base(destructor no virtual). ¿Por qué está bien?



inheritance (6)

Esta pregunta ya tiene una respuesta aquí:

Así que he estado usando un contenedor derivado de std :: vector durante algún tiempo. Quizás esta sea una decisión de diseño deficiente por varias razones, y la cuestión de si debe o no hacer tal cosa se ha discutido ampliamente aquí:

No heredarás de std :: vector

Subclase / heredar contenedores estandar?

¿Existe algún riesgo real de derivar de los contenedores STL de C ++?

¿Está bien heredar la implementación de los contenedores STL, en lugar de delegar?

Estoy seguro de que me he perdido algunas de las discusiones ... pero en los enlaces se encuentran argumentos razonables para ambos puntos de vista. Por lo que puedo decir, el "porque ~ vector () no es virtual" es la base de la "regla" de que no debe heredar de los contenedores STL. Sin embargo, si observo la implementación de std :: vector en g ++ 4.9.2, encuentro que std :: vector hereda de _Vector_base y _Vector_base un destructor no virtual.

template<typename _Tp, typename _Alloc = std::allocator<_Tp> > class vector : protected _Vector_base<_Tp, _Alloc> { ... ~vector() _GLIBCXX_NOEXCEPT { std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish, _M_get_Tp_allocator()); } ... }

dónde:

template<typename _Tp, typename _Alloc> struct _Vector_base { ... ~_Vector_base() _GLIBCXX_NOEXCEPT { _M_deallocate(this->_M_impl._M_start, this->_M_impl._M_end_of_storage - this->_M_impl._M_start); } ... }

Por lo tanto, la implementación de gcc 4.9.2 de std :: vector se hereda de las clases base con un destructor no virtual. Esto me lleva a creer que es una práctica aceptable. ¿Por qué está bien? ¿Cuáles son las condiciones específicas por las cuales tal práctica no es peligrosa?


1: std::vector no hereda de _Vector_base

O mejor dicho, no en mi biblioteca estándar de implementación. libc ++ implementa el vector así:

namespace std { template <class T, class Allocator = allocator<T> > class vector { ...

Claro, su implementación puede heredar de una clase base, pero mi implementación no lo hace. Esto lleva al primer error de su interpretación de la "regla":

La biblioteca estándar de C ++ se puede implementar de varias maneras, y realmente no podemos hacer suposiciones o declaraciones amplias y generales sobre la biblioteca estándar (la definida por ISO/IEC 14882:2014 ) desde una implementación de la misma.

Entonces, antes de seguir avanzando, recordemos: no nos centraremos en una sola implementación en esta respuesta. Vamos a considerar todas las implementaciones (centrándonos en seguir las definiciones del estándar C ++, no las definiciones de un archivo de encabezado particular en su disco duro).

2: Está bien, sí, entonces std::vector hereda de _Vector_base

Pero antes de continuar, admitamos que su implementación puede heredar de _Vector_base , y eso está bien. Entonces, ¿por qué se permite que std::vector herede de una clase base, pero no se le permite heredar de std::vector ?

3: Puedes heredar de std::vector , si quieres (solo ten cuidado)

std::vector puede heredar de _Vector_base por las mismas razones que puede heredar de std::vector : los implementadores de la biblioteca son muy cuidadosos (y usted también debería serlo).

std::vector no se delete d polimórficamente con un puntero _Vector_base . Así que no tenemos que preocuparnos por que ~vector() no sea virtual. Si no delete su clase heredada con un puntero polimérico std::vector , entonces ~vector() no virtual no es un problema. Está bien. Sin preocupaciones. No nos preocupemos por eso. La no delete polimórfica significa que no tenemos que preocuparnos de que los destructores sean virtuales o no.

Pero más allá de esto, los implementadores de la biblioteca se han asegurado de que std::vector nunca se _Vector_base usando una referencia _Vector_base . Lo mismo para usted: si puede asegurarse de que su clase de vector personalizado nunca se divida (por alguien que usa una referencia std::vector estándar), entonces también está bien aquí. No cortar significa que no tenemos que preocuparnos de que se hagan malas copias.

4: Por qué no deberías heredar de std::vector

Esto se ha respondido mucho en otras preguntas, pero lo diré de nuevo aquí (y con el enfoque de todo el _Vector_base herencia _Vector_base ): usted (probablemente) no debería heredar de std::vector .

El problema es que si lo hace, alguien que use su clase de vector personalizado podría borrarlo polimórficamente (podría tener un puntero std::vector en él, y delete , que es una cosa mala si lo hacen). O podrían tener una referencia std::vector a su objeto personalizado e intentar hacer una copia de él (lo que dividiría el objeto, lo que probablemente también sería Una cosa mala) (Sigo asumiendo una referencia std::vector a su se necesita un objeto personalizado para provocar el corte de objetos al copiar, porque sigo asumiendo que usted es lo suficientemente cuidadoso como para nunca cortar accidentalmente un objeto con una asignación de función o función descuidada, nunca sería tan descuidado, estoy seguro, pero alguien más con una std::vector referencia podría ser (y sí, estoy siendo un poco gracioso aquí)).

Puedes controlar lo que haces con tu código. Y si puede controlarlo (con cuidado) lo suficiente como para asegurarse de que no haya delete polimórficas ni cortes de objetos, está bien.

Pero a veces realmente no puedes controlar lo que otros hacen con tu código. Si está trabajando en un equipo, esto podría ser problemático si un miembro del equipo hace una de estas cosas sin saberlo. Por eso sigo mencionando el "cuidado".

Peor aún, si su código es usado por un cliente, realmente no puede controlar lo que hacen, y si hacen una de estas cosas malas, usted será el único a quien se le va a culpar y encargarse de solucionarlo ( diviértete refactorizando todo tu código que solía depender de tu clase personalizada heredada de std::vector ) (o simplemente dile al cliente que no puede hacer eso, pero es probable que tengas un cliente malhumorado que perdió el tiempo en la depuración de un problema extraño no esperaban encontrarse con).

Sin embargo, los implementadores de la biblioteca estándar de C ++ pueden salir adelante con esta herencia, ya que pueden controlar las cosas muy bien: a nadie se le permite usar _Vector_base . Puedes usar std::vector . Sólo std::vector . Dado que no está permitido (o nunca ) usar _Vector_base , los implementadores de bibliotecas estándar no tienen que preocuparse por el corte de objetos o las delete polimórficas. Y como están siendo muy cuidadosos en su implementación en un entorno controlado, las cosas funcionan bien.

Pero aún mejor, al no hacer suposiciones acerca de cómo se implementa std::vector , y en lugar de tratarlo como una caja negra (útil), podemos asegurarnos de que usamos std::vector portátil a otras implementaciones de bibliotecas estándar. Si realiza suposiciones acerca de std::vector heredado de alguna clase base, limitará la portabilidad de su código.


El problema que la regla "nunca heredar de un tipo sin un destructor virtual" intenta resolver es este:

  • Siempre que delete un objeto sin un destructor virtual, el destructor al que se llama no depende del tipo dinámico.
    Es decir, si el tipo declarado del puntero que delete difiere del tipo dinámico del objeto, se llama al destructor incorrecto.

Prohibir la subclasificación de tipos sin un destructor virtual es equivalente a restringir estos tipos a clases independientes: clases sin un padre y sin subclases derivadas. Es obvio que el tipo declarado de un puntero a una de estas clases nunca puede ser diferente del tipo dinámico del objeto, por lo tanto, no hay peligro de llamar al destructor incorrecto.

Sin embargo, esta regla es un poco demasiado estricta: lo que debe evitarse es llamar al destructor incorrecto, no hacer subclases de sí mismo. Aquí hay una jerarquía de clases que no permite llamar al destructor incorrecto:

class Foo { protected: Foo(); ~Foo(); }; class Bar : public Foo { public: Bar(); ~Bar(); };

Con estas dos clases, es perfectamente posible crear un objeto Bar y manipularlo a través de un puntero o referencia Foo ; sin embargo, no se puede usar un Foo* para destruir el objeto; la clase Foo sí misma no es instanciable ni desestabilizable desde otro código. .

Ahora volvamos a std::vector<> . La clase std::vector<> es la que debe utilizarse y no tiene un destructor virtual. Pero eso no requiere que no haya una clase base para std::vector<> . Una biblioteca es libre de implementar std::vector<> al dividir su implementación en dos clases, siguiendo el patrón de Foo y Bar arriba. Lo único que debe evitarse es que un usuario utilice un puntero de clase base para destruir un objeto derivado. Por supuesto, el tipo _Vector_base es privado para la implementación de las bibliotecas estándar, y los usuarios nunca deben usarlo, por lo que está bien.

Sin embargo, la subclase std::vector<> es una historia completamente diferente: el destructor de std::vector<> es público, por lo que no puede detener a los usuarios de una clase

template <class T> class MyVector : public std::vector<T> { ... };

para usar un puntero de clase base podrían obtener para destruir un MyVector<> . Podría usar la herencia privada o protegida para evitar el problema, pero eso no proporciona los beneficios de la herencia pública. Entonces, sí, nunca debe std::vector<> subclase std::vector<> aunque está perfectamente bien que std::vector<> herede de otra clase privada.


La respuesta tiene 2 partes:

  1. Porque _Vector_base sabía que iba a ser heredado por std::vector y fue diseñado como tal

  2. Hay una excepción a cada regla. Si heredar de std::vector tiene sentido en su caso, simplemente haga lo que tenga sentido.


No existe tal regla "no heredar de clases base con destructores no virtuales". Si hay una regla, será: "si tiene incluso un método virtual, haga que su destructor sea virtual, lo que también significa que no hereda de las clases base con destructores no virtuales". En mi humilde opinión está bien heredar contenedores stl si sigues esto.

Parece que algunos arquitectos compiladores también están de acuerdo con esto. Hay, literalmente, una advertencia que lo indica, para referencia: ¿Qué significa "tiene el método virtual ... pero la advertencia de un destructor no virtual" durante la compilación de C ++?


Porque es ilegal que el código de usuario intente destruir una _Vector_base , ya que es un tipo interno stdlib. Esto evita cualquier problema con los destructores. Lo mismo no se puede decir de ti.

En pocas palabras, los internos de la biblioteca estándar son un caso especial, tanto en las reglas de idioma como en lo que es razonable para ellos. No puedes generalizar de lo que hacen a tu propio código.


Sólo un punto de datos.

En "A Tour of C ++" Bjarne Stroustrup define una plantilla de clase que deriva de std::vector<T>. El propósito es tener una verificación de rango cuando se usa el operador []. Todo lo demás está delegado a la clase base.