C++: Heredando de std:: map
inheritance stdmap (4)
Quiero heredar de std::map
, pero hasta donde sé std::map
no tiene ningún destructor virtual.
Por lo tanto, ¿es posible llamar explícitamente al destructor de std::map
en mi destructor para garantizar la destrucción correcta de los objetos?
Quiero heredar de
std::map
[...]
Por qué ?
Hay dos razones tradicionales para heredar:
- reutilizar su interfaz (y por lo tanto, los métodos codificados en su contra)
- reutilizar su comportamiento
El primero no tiene sentido aquí, ya que el map
no tiene ningún método virtual
, por lo que no puede modificar su comportamiento heredando; y el último es una perversión del uso de la herencia que al final solo complica el mantenimiento.
Sin una idea clara de su uso previsto (falta de contexto en su pregunta), supongo que lo que realmente desea es proporcionar un contenedor similar a un mapa, con algunas operaciones de bonificación. Hay dos maneras de lograr esto:
- composición: crea un nuevo objeto, que contiene un
std::map
, y proporciona la interfaz adecuada - extensión: creas nuevas funciones gratuitas que operan en
std::map
Este último es más simple, pero también es más abierto: la interfaz original de std::map
todavía está abierta de par en par; por lo tanto, no es adecuado para restringir las operaciones.
El primero es más pesado, sin duda, pero ofrece más posibilidades.
Depende de usted decidir cuál de los dos enfoques es más adecuado.
@Matthieu M que dijiste
Quiero heredar de std :: map [...]
Por qué ?
Hay dos razones tradicionales para heredar:
- reutilizar su interfaz (y por lo tanto, los métodos codificados en su contra)
- reutilizar su comportamiento
El primero no tiene sentido aquí, ya que el mapa no tiene ningún método virtual, por lo que no puede modificar su comportamiento heredando; y el último es una perversión del uso de la herencia que al final solo complica el mantenimiento.
Respecto a "el primero":
La función clear()
es virtual, y para mí tiene mucho sentido que un std::map<key,valueClass*>::clear()
se sobrescriba en una clase derivada con un iterador que elimina todos los std::map<key,valueClass*>::clear()
apuntados instancias de la clase de valor antes de llamar a la clase base clear()
para evitar pérdidas de memoria accidentales, y es un truco que realmente he usado. En cuanto a por qué alguien querría usar un mapa para apuntar a las clases, el polimorfismo y las referencias no se pueden reasignar significa que no se puede usar en un contenedor de STL. En su lugar, puede sugerir el uso de un puntero inteligente o de referencia como el shared_ptr
(características de C ++ 11), pero cuando escribe una biblioteca, quiere que alguien restringido a un compilador de C ++ 98 pueda usar, no es una opción, a menos que vaya a imponer un requisito de impulso, lo que también puede ser indeseable. Y si realmente desea que el mapa tenga la propiedad exclusiva de su contenido, entonces no desea utilizar el paquete de referencia o la mayoría de las implementaciones de punteros inteligentes.
Respecto al "último":
Si desea un mapa para los punteros que las eliminaciones automáticas apuntan a la memoria, luego reutilizar "todos" el otro comportamiento del mapa y anular el borrado tiene mucho sentido para mí, por supuesto, también querrá anular los constructores de asignación / copia para clonar el señaló los objetos cuando copia el mapa para que no elimine dos veces una instancia apuntada a la valueClass
.
Pero eso solo requiere una cantidad extremadamente pequeña de codificación para implementar.
También uso un typedef std::map<key,valueClass*> baseClassMap;
protegido typedef std::map<key,valueClass*> baseClassMap;
como las 2 primeras líneas de la declaración del mapa de clase derivado, de modo que puedo llamar a baseClassMap::clear();
En la función clear()
anulada después del bucle del iterador, se valueClass*
todas las instancias de valueClass*
contenidas en el mapa derivado, lo que facilita el mantenimiento en caso de que el tipo de valueClass*
cambie.
El punto es que , si bien puede tener una aplicabilidad limitada en las buenas prácticas de codificación, no creo que sea justo decir que NUNCA es una buena idea descender del mapa. Pero tal vez tenga una mejor idea de que no he pensado en cómo lograr el mismo efecto de administración automática de la memoria sin agregar una cantidad significativa de código fuente adicional (por ejemplo, agregando un std::map
).
El destructor es llamado, incluso si no es virtual, pero ese no es el problema.
Obtendrá un comportamiento indefinido si intenta eliminar un objeto de su tipo a través de un puntero a un std::map
.
Use la composición en lugar de la herencia, los contenedores std
no están destinados a ser heredados, y usted no debería.
Supongo que desea ampliar la funcionalidad de std::map
(digamos que desea encontrar el valor mínimo), en cuyo caso tiene dos opciones mucho mejores y legales :
1) Como se sugiere, puede utilizar la composición en su lugar:
template<class K, class V>
class MyMap
{
std::map<K,V> m;
//wrapper methods
V getMin();
};
2) Funciones gratuitas:
namespace MapFunctionality
{
template<class K, class V>
V getMin(const std::map<K,V> m);
}
Hay una idea errónea: herencia -fuera del concepto de OOP pura, que C ++ no es - no es más que una "composición con un miembro sin nombre, con capacidad de decaimiento".
La ausencia de funciones virtuales (y el destructor no es especial, en este sentido) hace que su objeto no sea polimórfico, pero si lo que está haciendo es simplemente "reutilizar su comportamiento y exponer la interfaz nativa", la herencia hace exactamente lo que pidió.
Los destructores no necesitan ser llamados explícitamente entre sí, ya que su llamada siempre está encadenada por especificaciones.
#include <iostream>
unsing namespace std;
class A
{
public:
A() { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
void hello() { cout << "A::hello()" << endl; }
};
class B: public A
{
public:
B() { cout << "B::B()" << endl; }
~B() { cout << "B::~B()" << endl; }
void hello() { cout << "B::hello()" << endl; }
};
int main()
{
B b;
b.hello();
return 0;
}
saldrá
A::A()
B::B()
B::hello()
B::~B()
A::~A()
Haciendo A incrustado en B con
class B
{
public:
A a;
B() { cout << "B::B()" << endl; }
~B() { cout << "B::~B()" << endl; }
void hello() { cout << "B::hello()" << endl; }
};
que saldrá exactamente igual.
La regla "No derivar si el destructor no es virtual" no es una consecuencia obligatoria de C ++, sino solo una regla comúnmente aceptada no escrita (no hay nada en la especificación al respecto: aparte de una UB que llama eliminar en una base) que surge antes de C ++ 99, cuando OOP por herencia dinámica y funciones virtuales era el único paradigma de programación compatible con C ++.
Por supuesto, muchos programadores de todo el mundo hicieron sus huesos con ese tipo de escuela (lo mismo que enseñan iostreams como primitivos, luego se mueven hacia matrices e indicadores, y en la última lección el profesor dice "oh ... esto también es el STL que tiene vector, cadena y otras características avanzadas ") y en la actualidad, incluso si C ++ se convirtió en multiparadigm, sigue insistiendo con esta regla OOP pura.
En mi muestra, A :: ~ A () no es virtual exactamente como A :: hola. Qué significa eso?
Simple: por la misma razón, llamar a A::hello
no resultará en llamar a B::hello
, llamar a A::~A()
(por eliminar) no dará como resultado B::~B()
. Si puede aceptar -en su estilo de programación- la primera afirmación, no hay razón para que no pueda aceptar la segunda . En mi muestra no hay A* p = new B
que recibirá la delete p
ya que A :: ~ A no es virtual y sé lo que significa .
Exactamente la misma razón que no se realizará, usando el segundo ejemplo para B, A* p = &((new B)->a);
con un delete p;
Aunque este segundo caso, perfectamente dual con el primero, no parece interesante para nadie sin razones aparentes.
El único problema es el "mantenimiento", en el sentido de que, si un programador OOP ve el código yopur, lo rechazará, no porque sea incorrecto en sí mismo, sino porque se le ha dicho que lo haga.
De hecho, "no se deriva si el destructor no es virtual" se debe a que la mayoría de los programadores creen que hay demasiados programadores que no saben que no pueden llamar a eliminar de un puntero a una base . (Lo siento si esto no es cortés, pero después de más de 30 años de experiencia en programación, ¡no veo ninguna otra razón!)
Pero tu pregunta es diferente:
Llamar a B :: ~ B () (eliminando o finalizando el alcance) siempre resultará en A :: ~ A () ya que A (ya sea incrustado o heredado) es en cualquier caso parte de B.
Siguiendo los comentarios de Luchian: el comportamiento indefinido aludido anteriormente en sus comentarios se relaciona con una eliminación en una base de puntero a objeto sin destructor virtual.
Según la escuela OOP, esto da como resultado la regla "no se deriva si no existe un destructor virtual".
Lo que estoy señalando aquí es que las razones de esa escuela dependen del hecho de que cada objeto orientado a la OOP tiene que ser polimórfico y que todo es polimórfico debe ser direccionado por un puntero a una base, para permitir la sustitución del objeto. Al hacer esas afirmaciones, esa escuela está tratando deliberadamente de anular la intersección entre derivadas y no reemplazables, de modo que un programa puro de OOP no experimentará esa UB.
Mi posición, simplemente, admite que C ++ no es solo OOP, y que no todos los objetos de C ++ TIENEN QUE estar orientados a OOP de forma predeterminada, y admitir OOP no siempre es una necesidad, también admite que la herencia de C ++ no siempre es necesariamente necesaria para OOP sustitución.
std :: map NO es polimórfico por lo que NO es reemplazable. MyMap es el mismo: NO polimórfico y NO reemplazable.
Simplemente tiene que reutilizar std :: map y exponer la misma interfaz std :: map. Y la herencia es la manera de evitar una larga lista de funciones reescritas que solo llaman a las reutilizadas.
MyMap no tendrá dtor virtual ya que std :: map no tiene uno. Y esto, para mí, es suficiente para decirle a un programador de C ++ que estos no son objetos polimórficos y que no deben usarse uno en lugar del otro.
Debo admitir que esta posición no es hoy compartida por la mayoría de los expertos en C ++. Pero creo que (mi única opinión personal) es solo por su historia, que se relaciona con la POO como un dogma para servir, no por una necesidad de C ++. Para mí, C ++ no es un lenguaje OOP puro y no necesariamente debe seguir siempre el paradigma OOP, en un contexto donde no se sigue o requiere OOP.