iterador español cplusplus c++ inheritance stl

español - map c++



¿Hay algún riesgo real derivado de los contenedores C++ STL? (8)

¿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, está el excelente punto de Mankarse:

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

En segundo lugar, digamos que llaman f(*p1); donde f está especializado en std::vector<double> : no se encontrará esa especialización vector ; puede terminar haciendo coincidir la especialización de la plantilla de manera diferente; por ejemplo, código genérico corriente (más lento o menos eficiente) u obteniendo un error de enlazador si una versión no especializada no está realmente definida. A menudo no es una preocupación importante.

Personalmente, considero que la destrucción a través de un puntero a la base está cruzando la línea; puede ser solo un problema "hipotético" (por lo que usted puede ver) dado su compilador actual, indicadores del compilador, programa, versión del sistema operativo, etc., pero podría romperse en cualquier momento sin razón "buena".

Si está seguro de que puede evitar la eliminación a través de un puntero de clase base, vaya por ello.

Dicho eso, algunas notas sobre su evaluación:

  • "proporcionar implementaciones triviales de constructores" - eso 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) { } ... veces puede ser más fácil que enumerar todas las sobrecargas, pero no maneja parámetros no const , valores predeterminados, constructores explícitos vs no explícitos , ni escalar a un gran número de argumentos. C ++ 11 proporciona una mejor solución general.

Sin lugar a dudas, agregar un destructor a la class Rates o class Charges 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.

  • No hay ningún riesgo planteado por un destructor de clase derivado 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.)

  • Decir "invocará el destructor de la clase base" hace que suene como si se hiciera directamente sin un destructor de clase derivada definido implícitamente o haciendo la llamada, todo un detalle de optimización y no especificado por el Estándar.

La afirmación de que es un error utilizar alguna vez un contenedor estándar de C ++ como clase base me sorprende.

Si no es un abuso del idioma declarar ...

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

... entonces, ¿cuál es exactamente el peligro al declarar ...

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

Las ventajas positivas para B incluyen:

  • Habilita la sobrecarga de funciones porque f (Rates &) yf (Charges &) son firmas distintas
  • Permite que otras plantillas se especialicen, porque X <Rates> y X <Charges> son tipos distintos
  • La declaración directa es trivial
  • El depurador probablemente le dice si el objeto es un Tarifas o un Cargos
  • Si, a medida que pasa el tiempo, las Tarifas y los Cargos desarrollan personalidades, un Singleton for Rates, un formato de salida para Cargos, existe un alcance obvio para que se implemente esa funcionalidad.

Las ventajas positivas para A incluyen:

  • No tiene que proporcionar implementaciones triviales de constructores, etc.
  • El compilador pre-estándar de quince años que es lo único que compilará tu legado no ahogará
  • Dado que las especializaciones son imposibles, la plantilla X <Rates> y la plantilla X <Charges> usarán el mismo código, por lo que no habrá ningún tipo de hinchazón sin sentido.

Ambos enfoques son superiores al uso de un contenedor sin formato, porque si la implementación cambia de vector <double> a vector <float>, solo hay un lugar para cambiar con B y tal vez solo 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 y respondible, no una discusión de una práctica mejor o peor. 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, agregar un destructor a la clase Tasas 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:

Considere 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 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 la implementación de Microsoft, el vector <T> se implementa a través de la herencia. el vector <T, A> es un derivado público de _Vector_Val <T, A> ¿Debería preferirse la contención?


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


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

Las tasas, y los Cargos para ese asunto, SON IGUALES como 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 está la afirmación de que las tasas SON TODAVÍA IGUALES como un vector de dobles en este punto? Un vector de dobles no es un singleton por ejemplo, por lo tanto, si uso tus Rates para declarar mi vector de dobles para widgets, es posible que sufra un dolor de cabeza debido a tu código. ¿Qué más sobre tarifas y cargos están sujetos a cambios? ¿Alguno de los cambios de la clase base está aislado 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 quieres decir y decir lo que dices 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.


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, he descubierto que no es muy complicado crear mis propias clases de listas personalizadas con solo los métodos que mi código necesita definir (y un miembro privado de la clase "padre"). De hecho, a menudo conduce a clases mejor diseñadas.


Los contenedores estándar no tienen destructores virtuales, por lo que no puedes 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.


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 obtener más información, lea el artículo "¿Por qué no deberíamos heredar una clase de las clases de STL?"

Guía

Una clase base debe tener:

  • un destructor virtual público
  • o un destructor no virtual protegido

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