2012-05-07 25 views
18

Quiero heredar de std::map, pero hasta donde sé std::map no tiene ningún destructor virtual.C++: Heredar de std :: map

¿Es posible por lo tanto llamar al destructor std::map explícitamente en mi destructor para asegurar la destrucción adecuada del objeto?

Respuesta

28

Se llama al destructor, incluso si no es virtual, pero ese no es el problema.

Obtendrá un comportamiento indefinido si intenta eliminar un objeto de su tipo mediante un puntero a std::map.

Utilice la composición en lugar de la herencia, los contenedores std no están destinados a ser heredados, y no debe hacerlo.

Estoy asumiendo que desea ampliar la funcionalidad de std::map (decir que quiere encontrar el valor mínimo), en cuyo caso se tienen dos mucho mejor, y legal, opciones:

1) Como sugerido, 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 gratis:

namespace MapFunctionality 
{ 
    template<class K, class V> 
    V getMin(const std::map<K,V> m); 
} 
+9

+1 Siempre favorece la composición en lugar de la herencia. Aún así, desearía que hubiera alguna manera de reducir todo el código repetitivo necesario para envolver. – daramarak

+0

@daramarak: yo también, si algo como 'using attribute.insert;' pudiera funcionar! Por otro lado, es bastante raro que realmente necesites todos los métodos, y envolverlo te da la oportunidad de dar un nombre significativo y tomar tipos de nivel superior :) –

+3

@daramarak: * Todavía deseo que haya alguna forma de reducir todo el código repetitivo necesario para envolver *: sí, hay: herencia. Pero los programadores están convencidos de que no deberían usarlo ... porque siempre tienden a interpretarlo como "es una". Pero eso no es un requisito, solo una convención pública. –

13

quiero heredar de std::map [...]

¿Por qué?

Hay dos razones tradicionales para heredar:

  • volver a utilizar su interfaz (y por lo tanto, los métodos codificados en contra de ella)
  • volver a utilizar su comportamiento

El primero no tiene sentido aquí como 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 solo complica el mantenimiento al final.


Sin una idea clara de su uso previsto (falta de contexto en su pregunta), voy a suponer que lo que realmente quiere es proporcionar un recipiente en forma de mapa, con algunas operaciones de bonificación. Hay dos maneras de lograr esto:

  • composición: se crea un nuevo objeto, que contiene un std::map, y proporciona la interfaz adecuada
  • extensión: se crean nuevos lanzamientos de funciones que operan sobre std::map

Este último es más simple, sin embargo, también es más abierto: la interfaz original de std::map sigue abierta de par en par; por lo tanto, no es adecuado para restringir las operaciones.

El primero es más pesado, sin dudas, pero ofrece más posibilidades.

Depende de usted decidir cuál de los dos enfoques es más adecuado.

12

Existe un concepto erróneo: la herencia, fuera del concepto de POO puro, que C++ no lo 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 "reutilizarlo y exponer la interfaz nativa", la herencia hace exactamente lo que preguntó.

Los destructores no necesitan ser llamados explícitamente entre sí, ya que su llamada siempre está encadenada por especificación.

#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; 
} 

es la salida

A::A() 
B::B() 
B::hello() 
B::~B() 
A::~A() 

de tomar una 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 dará salida a exactamente la misma.

El "No deriva si el destructor no es virtual" no es una consecuencia obligatoria de C++, sino simplemente una regla no escrita comúnmente aceptada (no hay nada en la especificación: aparte una UB llamando a 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 (el mismo que enseñan iostreams como primitivas, a continuación, se mueve a la matriz y punteros, y en la última lección que el maestro dice "oh. .. tehre es también el STL que tiene vector, cadena y otras características avanzadas ") y hoy, incluso si C++ se convirtió en multiparadigm, aún insiste con esta regla OOP pura.

En mi muestra A :: ~ A() no es virtual exactamente como A :: hello. Qué significa eso?

simple: por la misma razón llamando A::hello no dará lugar a llamar B::hello, llamando A::~A() (por delete) no dará lugar a B::~B(). Si puede aceptar -en su estilo de programación- la primera afirmación, no hay ninguna razón por la que no pueda aceptar el segundo. En mi muestra no hay A* p = new B que recibirá delete p ya que A :: ~ A no es virtual y Sé lo que significa.

Exactamente esa misma razón que no hará, utilizando el segundo ejemplo para B, A* p = &((new B)->a); con un , aunque este segundo caso, perfectamente dual con el primero, no parece interesante a 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 lo han indicado.

De hecho, el "no deriva si el destructor no es virtual" es porque la mayoría de los programadores creen que hay demasiados programadores que no saben que no pueden llamar eliminar en un puntero a una base. (Lo siento si esto no es de buena educación, pero después de 30 años de experiencia en programación no puede ver ninguna otra razón!)

Pero su pregunta es diferente:

Calling B :: ~ B() (por eliminación o por alcance del alcance) siempre dará como resultado A :: ~ A() ya que A (ya sea incrustado o heredado) es en cualquier caso parte de B.


Tras los comentarios Luchian: el comportamiento Indefinido aludido por encima de una de sus comentarios se relaciona con una deleción en un an-object's base puntero a 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 OOP tiene que ser polimórfico y todo es polimórfico debe ser direccionable por puntero a una base, para permitir la sustitución de objetos . Al hacer esas afirmaciones, esa escuela intenta deliberadamente anular la intersección entre derivado y no reemplazable, de modo que un programa OOP puro no experimentará ese UB.

Mi posición, simplemente, admite que C++ no es solo OOP, y no todos los objetos de C++ TENGO QUE ORIENTARSE de manera predeterminada, y admitir OOP no siempre es una necesidad necesaria, también admite que la herencia de C++ no siempre es necesariamente sirviendo a la sustitución OOP.

std :: map NO es polimórfico, por lo que NO es reemplazable. MyMap es lo mismo: NO polimórfico y NO reemplazable.

Simplemente tiene que volver a usar std :: map y exponer la misma interfaz std :: map. Y la herencia es solo la forma de evitar una larga repetición de funciones reescritas que solo llama a las reutilizadas.

MyMap no tendrá controlador 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.

Tengo que admitir que esta posición no la comparten actualmente la mayoría de los expertos en C++. Pero creo (mi única opinión personal) esto es solo debido a su historia, que se relacionan con OOP como un dogma para servir, no debido a una necesidad de C++. Para mí, C++ no es un lenguaje POO puro y no siempre debe seguir necesariamente el paradigma de POO, en un contexto donde no se sigue o no se requiere POO.

+3

Aquí está haciendo algunas declaraciones peligrosas. No considere obsoleta la necesidad de un destructor virtual. El estándar ** establece claramente ** que el comportamiento indefinido surge en la situación que mencioné. La abstracción es una gran parte de OOP. Eso significa que no solo deriva para reutilizar, sino también para ocultar el tipo real. Es decir, en un buen diseño, si usas herencia, terminarás con 'std :: map *' que en realidad apunta a 'MyMap'. Y si lo elimina, cualquier cosa puede suceder, incluido un bloqueo. –

+4

@LuchianGrigore: * La norma establece claramente que el comportamiento indefinido surge en la situación que mencioné. *. Es cierto, pero esta no es la situación que mencioné, y no es en la que está el OP. * Es decir, en un buen diseño, si usa herencia, terminará con std :: map * que en realidad apunta a MyMap *: esto es en general FALSE, y verdadero solo con POO puro basado en puntero. Eso es exactamente lo que mis muestras NO son. ¿Cómo se explica la existencia de mis muestras, que no usan polimorfismo y punteros en absoluto? –

+2

@LuchianGrigore: De todos modos, creo que eres * correcto *: lo que estoy afirmando ES peligroso, pero no para la corrección del programa, ¡sino para la cultura basada en programación OOP! Pero no se preocupe: ¡se esperaba su reacción! –

Cuestiones relacionadas