2009-05-28 8 views
29

Me sorprende la afirmación de que es un error utilizar alguna vez un contenedor estándar de C++ como clase base.¿Hay algún riesgo real derivado de los contenedores C++ STL?

Si no es el abuso de la lengua para declarar ...

// Example A 
typedef std::vector<double> Rates; 
typedef std::vector<double> Charges; 

... entonces, ¿qué es, exactamente, el peligro en la declaración de ...

// Example B 
class Rates : public std::vector<double> { 
    // ... 
} ; 
class Charges: public std::vector<double> { 
    // ... 
} ; 

Las ventajas positivas a B incluyen:

  • Habilita la sobrecarga de funciones porque f (la tarifa &) y F (Cargos &) son distintas firmas
  • Habilita otras plantillas que ser especializados, debido X <tarifas> y X <Cargos> son tipos distintos
  • declaración adelantada es trivial
  • depurador probablemente le indica si el objeto es un tarifas o una Cargos
  • Si, con el paso del tiempo, las Tarifas y Cargos desarrollan personalidades — un Singleton for Rates, un formato de salida para los Cargos — existe un alcance obvio para que se implemente esa funcionalidad.

Las ventajas positivas a A incluyen:

  • no tienen que proporcionar implementaciones triviales de constructores etc
  • El compilador de quince años de edad pre-estándar que es el único que va a compilar su legado no estrangula
  • Dado que las especializaciones son imposibles, la plantilla X < Las tasas > y la plantilla X Cargas > utilizarán el mismo código, por lo que no habrá ninguna hinchazón sin sentido.

Ambos enfoques son superiores a la utilización de un contenedor de crudo, ya que si los cambios en la implementación de vectores <doble> al vector <flotador>, sólo hay un lugar para cambiar con B y quizá sólo un lugar para cambiar con A (podría ser más, porque alguien puede haber puesto declaraciones typedef idénticas en múltiples lugares).

Mi objetivo es que esta sea una pregunta específica, responable, no una discusión de mejores o peores prácticas. Muestre lo peor que puede suceder como consecuencia de derivarse de un contenedor estándar, que se hubiera evitado utilizando un typedef en su lugar.

Editar:

Sin lugar a dudas, la adición de un destructor de clase de tarifas o cargos de clase sería un riesgo, porque std :: vector no declara su destructor como virtual. No hay destructor en el ejemplo, y no hay necesidad de uno. La destrucción de un objeto Rates o Charges invocará al destructor de la clase base. No hay necesidad de polimorfismo aquí, tampoco. El desafío es mostrar que algo malo sucede como consecuencia del uso de la derivación en lugar de un typedef.

Editar:

consideran este caso de uso:

#include <vector> 
#include <iostream> 

void kill_it(std::vector<double> *victim) { 
    // user code, knows nothing of Rates or Charges 

    // invokes non-virtual ~std::vector<double>(), then frees the 
    // memory allocated at address victim 
    delete victim ; 

} 

typedef std::vector<double> Rates; 
class Charges: public std::vector<double> { }; 

int main(int, char **) { 
    std::vector<double> *p1, *p2; 
    p1 = new Rates; 
    p2 = new Charges; 
    // ??? 
    kill_it(p2); 
    kill_it(p1); 
    return 0; 
} 

¿Hay cualquier posible error que incluso un usuario arbitrariamente desafortunado podría introducir en el ??? sección que dará como resultado un problema con Cargos (la clase derivada), pero no con Tasas (el typedef)?

En la implementación de Microsoft, el vector <T> se implementa a través de la herencia. vector < T, A > es un derivado público de _Vector_Val < T, A > ¿Debería ser preferible la contención?

+4

El comentario en 'kill_it' es incorrecto. Si el tipo dinámico de 'víctima' no es' std :: vector', entonces 'delete' simplemente invoca un comportamiento indefinido. La llamada a 'kill_it (p2)' hace que esto suceda, por lo que no es necesario agregar nada a la sección '// ???' para que esto tenga un comportamiento indefinido. – Mankarse

Respuesta

16

Porque necesita un destructor virtual y los contenedores estándar no lo tienen. Los contenedores estándar no están diseñados para actuar como clase base.

Para más información leer el artículo "Why shouldn't we inherit a class from STL classes?"

Pauta

Una clase base debe tener:

  • un destructor virtual pública
  • o un destructor no virtual protegida
+14

Esto se aplica cuando diseñas para el polimorfismo en tiempo de ejecución. No hay nada de malo en derivar de clases no virtuales si no tiene la intención de tratarlos como tipos de base distintos de un fuerte acoplamiento. –

+1

¿En qué parte del ejemplo B ve la necesidad de un destructor virtual? –

+1

No se puede decir del ejemplo B porque es la declaración de clase y no el uso de la clase. Un usuario CAN y WILL abusarán de la clase. Una clase base debe tener: - un destructor virtual público - o un destructor no virtual protegido – TimW

25

Los contenedores estándar no tienen destructores virtuales, por lo que no puede manejarlos polimórficamente. Si no lo hace, y cualquiera que use su código no lo hace, no es "incorrecto", per se. Sin embargo, es mejor utilizar la composición de todos modos, para mayor claridad.

+6

Al usar esta práctica con contenedores STL, he descubierto que la herencia privada es una forma sintácticamente más fácil de expresar la composición. – ASk

3

Además, en la mayoría de los casos, si es posible, debe preferir la composición o la agregación sobre la herencia.

+0

Dado que estrictamente hablando, siempre es posible simular herencia con contención; solo se necesita una gran cantidad de conversiones estáticas y dinámicas y un apropiado de operadores de conversión. ¿Esto significa que la herencia debe eliminarse del lenguaje? –

+1

@Thomas L Holaday Por supuesto que no; quizás debería aclarar. Si no va a especializarse en la clase base, sino que solo necesita usar las características de la clase base en su clase (como en el ejemplo), es mejor tener un miembro privado de esa clase en su clase. Todos los demás beneficios enumerados en la pregunta se obtienen simplemente al tener una clase, no el hecho de que heredó de cualquier otra clase particular. – Trent

5

El problema no es filosófico, es un problema de implementación. Los destructores de los contenedores estándar no son virtuales, lo que significa que no hay forma de usar polymorphisim en tiempo de ejecución para obtener el desctructor adecuado.

En la práctica, creo que no es muy complicado crear mis propias listas personalizadas con solo los métodos que mi código necesita (y un miembro privado de la clase "padre"). De hecho, a menudo conduce a clases mejor diseñadas.

6

Un fuerte contraargumento, en mi opinión, es que está imponiendo una interfaz y la implementación en sus tipos. ¿Qué sucede cuando descubres que la estrategia de asignación de memoria vectorial no se ajusta a tus necesidades? ¿Derivará del std:deque? ¿Y esas 128K líneas de código que ya usan tu clase? ¿Todos necesitarán recompilar todo? ¿Incluso compilará?

+2

¿Cómo usar la herencia más una imposición de interfaz e implementación que usar un typedef? –

+1

Bueno, no comparé uno con el otro, solo decía que la derivación pública es peligrosa en este sentido.Después de todo, puedes esconder un vector privado detrás de un pimpl, pero no puedes ocultar a tus antepasados ​​:) –

3

Aparte del hecho de que una clase base necesita un destructor virtual o un destructor no virtual protegida que está haciendo la siguiente afirmación en su diseño:

tarifas y cargos por eso importa, SON LOS MISMOS que un vector de dobles en su ejemplo anterior. Según su propia afirmación "... a medida que pasa el tiempo, las tasas y los cargos desarrollan personalidades ..."Entonces, la afirmación de que las tasas TODAVÍA SON LAS MISMAS que un vector de dobles en este punto? Un vector de dobles no es un singleton por ejemplo, por lo tanto si utilizo las tasas para declarar mi vector de dobles para widgets puedo incurrir en dolor de cabeza de su código. ¿Qué más sobre las tasas y los cargos están sujetos a cambios? ¿Alguno de los cambios de la clase base están aislados de forma segura de los clientes de su diseño si cambian de manera fundamental?

El punto es que una clase es un elemento , de muchos en C++, para expresar intenciones de diseño. Decir lo que quiere decir y decir lo que dice es la razón contra el uso de la herencia de esta manera.

... O simplemente publicado más sucintamente antes de mi respuesta: Sustitución

1

¿Hay algún error posible que incluso un usuario arbitrariamente desafortunado podría introducir en el ??? sección que dará como resultado un problema con Cargos (la clase derivada), pero no con Tasas (el typedef)?

En primer lugar, hay un excelente punto de Mankarse:

el comentario en kill_it está mal. Si el tipo dinámico de víctima no es std::vector, entonces el delete simplemente invoca un comportamiento indefinido. La llamada al kill_it(p2) hace que esto ocurra, por lo que no es necesario agregar nada a la sección //??? para que este tenga un comportamiento indefinido. - MankarseSep 3 '11 at 10:53

En segundo lugar, dicen que ellos llaman f(*p1); donde f está especializada para std::vector<double>: que no se encontrará vector especialización - que puede terminar el juego especialización de plantilla diferente - por lo general se ejecuta (más lento o de otra manera menos eficiente) código genérico u obtener un error de enlazador si una versión no especializada no está realmente definida. No suele ser una preocupación importante.

Personalmente, considero destrucción a través de un puntero a la base para ser cruzar la línea - que puede solamente ser un problema "hipotético" (por lo que se puede decir) dada su compilador actual, opciones del compilador, el programa, Versión del sistema operativo, etc., pero podría romperse en cualquier momento sin motivo "bueno".

Si está seguro de que puede evitar la eliminación a través de un puntero de clase base, anímese.

Dicho esto, algunas notas sobre su evaluación:

  • "proporcionar implementaciones triviales de constructores" - que es una molestia, pero un consejo para C++ 03: template <typename A> Classname(const A& a) : Base(a) { } template <typename A, typename B> Classname(const A& a, const B& b) : Base(a, b) { } ... a veces puede ser más fácil que la enumeración de todas las sobrecarga, pero no maneja parámetros que no sean const, valores predeterminados, constructores explícitos versus no explícitos, ni escala a un gran número de argumentos. C++ 11 proporciona una mejor solución general.

Sin lugar a dudas, la adición de un destructor de class Rates o class Charges habría un riesgo, porque std::vector no declaran su destructor como virtual. No hay destructor en el ejemplo, y no hay necesidad de uno. La destrucción de un objeto Rates o Charges invocará al destructor de la clase base. No hay necesidad de polimorfismo aquí, tampoco.

  • No hay riesgo planteado por un destructor clase derivada si el objeto no se elimina polimórficamente; si hay un comportamiento indefinido ya sea que su clase derivada tenga un destructor definido por el usuario o no. Dicho esto, se pasa de "probablemente-vale para un vaquero" a "casi-sin dudas-no-está bien" cuando se agregan miembros de datos o bases adicionales con destructores que realizan tareas de limpieza (desasignación de memoria, desbloqueo de mutex, archivo manejar el cierre, etc.)

  • Diciendo "invocará el destructor de la clase base" hace que parezca hecho directamente sin un destructor de clase derivada implícitamente involucrado o realizando la llamada, todo un detalle de optimización y no especificado por el Estándar.

Cuestiones relacionadas