¿Por qué debería declarar un destructor virtual para una clase abstracta en C ++?

165

Sé que es una buena práctica declarar destructores virtuales para clases base en C ++, pero ¿siempre es importante declarar virtualdestructores incluso para clases abstractas que funcionan como interfaces? Proporcione algunas razones y ejemplos de por qué.

Kevin
fuente

Respuestas:

196

Es aún más importante para una interfaz. Cualquier usuario de su clase probablemente mantendrá un puntero a la interfaz, no un puntero a la implementación concreta. Cuando vienen a eliminarlo, si el destructor no es virtual, llamarán al destructor de la interfaz (o al valor predeterminado proporcionado por el compilador, si no especificó uno), no al destructor de la clase derivada. Fuga de memoria instantánea.

Por ejemplo

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}
Airsource Ltd
fuente
44
delete pInvoca un comportamiento indefinido. No se garantiza llamar Interface::~Interface.
Mankarse
@Mankarse: ¿puedes explicar qué hace que sea indefinido? Si Derived no implementara su propio destructor, ¿seguiría siendo un comportamiento indefinido?
Ponkadoodle
14
@Wallacoloo: Se indefinido debido a [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Todavía estaría indefinido si Derived usara un destructor generado implícitamente.
Mankarse
37

La respuesta a su pregunta es a menudo, pero no siempre. Si su clase abstracta prohíbe a los clientes llamar a eliminar en un puntero (o si lo dice en su documentación), tiene la libertad de no declarar un destructor virtual.

Puede prohibir a los clientes que llamen a borrar en un puntero al hacer que su destructor esté protegido. Trabajando así, es perfectamente seguro y razonable omitir un destructor virtual.

Eventualmente terminará sin una tabla de métodos virtuales, y terminará señalando a sus clientes su intención de hacer que no se pueda eliminar a través de un puntero, por lo que tiene razones para no declararlo virtual en esos casos.

[Ver ítem 4 en este artículo: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
fuente
La clave para que su respuesta funcione es "en el que no se solicita eliminar". Por lo general, si tiene una clase base abstracta diseñada para ser una interfaz, se llamará a eliminar en la clase de interfaz.
John Dibling
Como John señaló anteriormente, lo que estás sugiriendo es bastante peligroso. Confía en la suposición de que los clientes de su interfaz nunca destruirán un objeto conociendo solo el tipo base. La única manera de garantizar que si no es virtual es proteger el dtor de la clase abstracta.
Michel
Michel, lo he dicho :) "Si haces eso, proteges tu destructor. Si lo haces, los clientes no podrán eliminar usando un puntero a esa interfaz". y, de hecho, no depende de los clientes, sino que debe imponerse diciéndoles a los clientes "no se puede hacer ...". No veo ningún peligro
Johannes Schaub - litb
arreglé la pobre redacción de mi respuesta ahora. lo declara explícitamente ahora que no depende de los clientes. En realidad, pensé que era obvio que confiar en los clientes para hacer algo está fuera de lugar de todos modos. gracias :)
Johannes Schaub - litb
2
+1 por mencionar destructores protegidos, que son la otra "solución" al problema de llamar accidentalmente al destructor incorrecto al eliminar un puntero a una clase base.
j_random_hacker
23

Decidí investigar un poco e intentar resumir tus respuestas. Las siguientes preguntas lo ayudarán a decidir qué tipo de destructor necesita:

  1. ¿Su clase está destinada a ser utilizada como una clase base?
    • No: declare un destructor público no virtual para evitar el puntero en V en cada objeto de la clase * .
    • Sí: lea la siguiente pregunta.
  2. ¿Es su clase base abstracta? (es decir, ¿algún método virtual puro?)
    • No: intente hacer que su clase base sea abstracta rediseñando su jerarquía de clases
    • Sí: lea la siguiente pregunta.
  3. ¿Desea permitir la eliminación polimórfica a través de un puntero base?
    • No: declare un destructor virtual protegido para evitar el uso no deseado.
    • Sí: declarar destructor virtual público (sin gastos generales en este caso).

Espero que esto ayude.

* Es importante tener en cuenta que no hay forma en C ++ de marcar una clase como final (es decir, no subclase), por lo que en el caso de que decida declarar su destructor no virtual y público, recuerde advertir explícitamente a sus compañeros programadores contra derivando de tu clase.

Referencias

davidag
fuente
11
Esta respuesta está parcialmente desactualizada, ahora hay una palabra clave final en C ++.
Étienne
10

Sí, siempre es importante. Las clases derivadas pueden asignar memoria o contener referencias a otros recursos que deberán limpiarse cuando se destruya el objeto. Si no proporciona a sus interfaces / clases abstractas destructores virtuales, cada vez que elimine una instancia de clase derivada a través de un identificador de clase base, no se llamará al destructor de su clase derivada.

Por lo tanto, está abriendo el potencial de pérdidas de memoria

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
DO
fuente
Es cierto, de hecho, en ese ejemplo, puede que no solo se pierda la memoria, sino que posiblemente se bloquee: - /
Evan Teran, el
7

No siempre es obligatorio, pero creo que es una buena práctica. Lo que hace es permitir que un objeto derivado se elimine de forma segura a través de un puntero de tipo base.

Así por ejemplo:

Base *p = new Derived;
// use p as you see fit
delete p;

está mal formado si Baseno tiene un destructor virtual, porque intentará eliminar el objeto como si fuera un Base *.

Evan Teran
fuente
¿no quieres arreglar boost :: shared_pointer p (new Derived) para que parezca boost :: shared_pointer <Base> p (new Derived); ? tal vez la gente entienda su respuesta y vote
Johannes Schaub - litb
EDITAR: "Codificó" un par de partes para hacer visibles los corchetes angulares, como sugiere litb.
j_random_hacker
@EvanTeran: no estoy seguro de si esto ha cambiado desde que se publicó originalmente la respuesta (la documentación de Boost en boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm sugiere que puede haber cambiado ), pero no es cierto en estos días que shared_ptrintentará eliminar el objeto como si fuera un Base *: recuerda el tipo de cosa con la que lo creó. Consulte el enlace al que se hace referencia, en particular el bit que dice "El destructor llamará a eliminar con el mismo puntero, completo con su tipo original, incluso cuando T no tiene un destructor virtual o está vacío".
Stuart Golodetz
@StuartGolodetz: Hmm, puede que tengas razón, pero honestamente no estoy seguro. Todavía puede estar mal formado en este contexto debido a la falta de destructor virtual. Vale la pena estudiarlo.
Evan Teran
@EvanTeran: en caso de que sea útil, stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz
5

No es solo una buena práctica. Es la regla # 1 para cualquier jerarquía de clases.

  1. La clase más básica de una jerarquía en C ++ debe tener un destructor virtual

Ahora para el por qué. Tome la típica jerarquía animal. Los destructores virtuales pasan por el despacho virtual como cualquier otra llamada a método. Toma el siguiente ejemplo.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Suponga que Animal es una clase abstracta. La única forma en que C ++ conoce el destructor adecuado para llamar es mediante el envío de método virtual. Si el destructor no es virtual, simplemente llamará al destructor de Animal y no destruirá ningún objeto en las clases derivadas.

La razón para hacer que el destructor sea virtual en la clase base es que simplemente elimina la elección de las clases derivadas. Su destructor se vuelve virtual por defecto.

JaredPar
fuente
2
Yo en su mayoría de acuerdo con usted, ya que por lo general cuando se define una jerarquía que desea ser capaz de hacer referencia a un objeto derivado usando un puntero de clase base / referencia. Pero ese no es siempre el caso, y en esos otros casos, puede ser suficiente proteger la clase base dtor en su lugar.
j_random_hacker
@j_random_hacker haciéndolo protegido no lo protegerá de eliminaciones internas incorrectas
JaredPar
1
@JaredPar: Eso es correcto, pero al menos puede ser responsable en su propio código; lo difícil es asegurarse de que el código del cliente no pueda hacer que su código explote. (Del mismo modo, hacer que un miembro de datos sea privado no evita que el código interno haga algo estúpido con ese miembro)
J_random_hacker
@j_random_hacker, lamento responder con una publicación de blog, pero realmente se ajusta a este escenario. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar
@JaredPar: Excelente publicación, estoy de acuerdo con usted al 100%, especialmente sobre la verificación de contratos en el código minorista. Solo quiero decir que hay casos en los que sabes que no necesitas un dtor virtual. Ejemplo: clases de etiquetas para despacho de plantillas. Tienen tamaño 0, solo se usa la herencia para indicar especializaciones.
j_random_hacker
3

La respuesta es simple, necesita que sea virtual; de lo contrario, la clase base no sería una clase polimórfica completa.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Preferiría la eliminación anterior, pero si el destructor de la clase base no es virtual, solo se llamará al destructor de la clase base y todos los datos en la clase derivada permanecerán sin recuperar.

fatma.ekici
fuente