programacion poo polimorfismo herencia ejemplos derivadas derivada clases clase c++ virtual destructor virtual-destructor

c++ - poo - Si cambio el destructor de una clase base de no virtual a virtual, ¿qué pasará?



herencia programacion (7)

Depende de lo que esté haciendo tu código, obviamente.

En términos generales, hacer que el destructor de la clase base sea virtual solo es necesario si tiene un uso como

Base *base = new SomeDerived; // whatever delete base;

Tener un destructor no virtual en Base hace que lo anterior muestre un comportamiento indefinido. Hacer el destructor virtual elimina el comportamiento indefinido.

Sin embargo, si haces algo como

{ // start of some block scope Derived derived; // whatever }

entonces no es necesario que el destructor sea virtual, ya que el comportamiento está bien definido (los destructores de Derived y sus bases se invocan en orden inverso a sus constructores).

Si cambiar el destructor de no virtual a virtual hace que falle un caso de prueba, entonces necesita examinar el caso de prueba para comprender por qué. Una posibilidad es que el caso de prueba se base en algún conjuro específico de comportamiento indefinido, lo que significa que el caso de prueba es defectuoso y puede no tener éxito en diferentes circunstancias (por ejemplo, compilar el programa con un compilador diferente). Sin ver el caso de prueba (o un MCVE que lo represente), sin embargo, dudaría en afirmar que SÍ confía en un comportamiento indefinido

Me encontré con una clase base cuyo destructor no es virtual, aunque la clase base tiene 1 función virtual fv() . Esta clase base también tiene muchas subclases. Muchas de esas subclases definen su propio fv() .

No sé los detalles de cómo se usan la base y las subclases en el programa. Solo sé que el programa funciona bien, incluso el destructor de la clase base debería ser virtual.

Quiero cambiar el destructor de la clase base de no virtual a virtual. Pero no estoy seguro de las consecuencias. Entonces, ¿qué pasará? ¿Qué más debo hacer para asegurarme de que el programa funcione bien después de cambiarlo?

Seguimiento: después de que cambié el destructor de la clase base de no virtual a virtual, el programa falló en un caso de prueba.
El resultado me confunde. Porque si el destructor de la clase base no es virtual, entonces el programa no usará polimorfismo de la clase base. Porque si no, lleva a un comportamiento indefinido. Por ejemplo, Base *pb = new Sub .
Entonces, creo que si cambio el destructor de no virtual a virtual, no debería causar más errores.


Echa un vistazo here ,

struct Component { int* data; Component() { data = new int[100]; std::cout << "data allocated/n"; } ~Component() { delete[] data; std::cout << "data deleted/n"; } }; struct Base { virtual void f() {} }; struct Derived : Base { Component c; void f() override {} }; int main() { Base* b = new Derived; delete b; }

La salida:

datos asignados

pero no eliminado

Conclusión

Cada vez que una jerarquía de clases tiene un estado, en el nivel puramente técnico, desea un destructor virtual desde la parte superior.

Es posible que una vez que haya agregado ese destructor virtual a su clase, haya activado una lógica de destrucción no probada. La opción sensata aquí es mantener el destructor virtual que ha agregado y corregir la lógica. De lo contrario, tendrá pérdidas de recursos y / o memoria en su proceso.

Detalles técnicos

Lo que sucede en el ejemplo es que, si bien Base tiene una vtable , su destructor en sí no es virtual, y eso significa que cada vez que se llama a Base::~Base() , no pasa por una vptr . En otras palabras, simplemente llama a Base::Base() , y eso es todo.

En la función main() , un nuevo objeto Derived se asigna y se asigna a una variable de tipo Base* . Cuando se ejecuta la siguiente instrucción de delete , en realidad primero intenta llamar al destructor del tipo directamente pasado, que es simplemente Base* , y luego libera la memoria ocupada por ese objeto. Ahora, como el compilador ve que Base::~Base() no es virtual, no intenta pasar por el vptr del objeto d . Esto significa que Derived::~Derived() nunca es llamado por nadie. Pero dado que Derived::~Derived() es donde el compilador generó la destrucción de Component Derived::c , ese componente tampoco se destruye nunca. Por lo tanto, nunca vemos datos borrados impresos.

Si Base::~Base() fuera virtual, lo que sucedería es que la instrucción delete d iría a través del vptr del objeto d , llamando al destructor, Derived::~Derived() . Ese destructor, por definición, primero llamaría Base::~Base() (esto es generado automáticamente por el compilador), y luego destruye su estado interno, es decir, el Component c . Por lo tanto, todo el proceso de destrucción se habría completado como se esperaba.


La virtualidad del destructor no romperá nada en el código existente a menos que haya otros problemas. Incluso puede resolver algunos (ver más abajo). Sin embargo, la clase puede no estar diseñada como polimórfica, por lo que la adición virtual a su destructor la habilita como polimórfica, lo que podría no ser deseable. Sin embargo, debería poder agregar virtualmente al destructor de manera segura y no debería causar problemas en sí mismo.

Explicación

El polimorfismo permite esto:

class A { public: ~A() {} }; class B : public A { ~B() {} int i; }; int main() { A *a = new B; delete a; }

Puede llevar el puntero al objeto de tipo A de la clase que es de hecho de tipo B Esto es útil, por ejemplo, para dividir interfaces (por ejemplo, A ) e implementaciones (por ejemplo, B ). Sin embargo, lo que sucederá en delete a; ?

Parte del objeto a de tipo A se destruye. Pero ¿qué pasa con la parte del tipo B ? Además esa parte tiene recursos y necesitan ser liberados. Bueno, eso es una pérdida de memoria allí. Al llamar a delete a; llama al destructor de tipo A (porque a es un puntero al tipo A ), básicamente llama a->~a(); . Destructor de tipo B nunca se llama. ¿Cómo resolver esto?

class A : { public: virtual ~A() {} };

Al agregar el despacho virtual al destructor de la A (tenga en cuenta que al declarar el destructor base virtual, los destructores de todas las clases derivadas se vuelven virtuales, incluso cuando no se declaran como tales). Entonces la llamada para delete a; enviará la llamada del destructor a la tabla virtual para encontrar el destructor correcto a utilizar (en este caso, del tipo B ). Ese destructor llamará destructores padres como de costumbre. Limpio, ¿verdad?

Posibles problemas

Como puedes ver al hacer esto, no puedes romper nada per se. Sin embargo, puede haber diferentes problemas en su diseño. Por ejemplo, podría haber un error que se "confió" en la llamada no virtual del destructor que expusiste al hacerla virtual, considera:

int main() { B *b = new B; A *a = b; delete a; b->i = 10; //might work without virtual destructor, also undefined behvaiour }

Básicamente, la segmentación de objetos, pero como no tenía un destructor virtual antes de eso, B parte del objeto creado no se destruyó, por lo que la asignación a i podría funcionar. Si hizo el destructor virtual, entonces no existe y es probable que se bloquee o haga lo que sea (comportamiento indefinido).

Cosas como estas pueden suceder y en código complicado puede ser difícil de encontrar. Pero si su destructor provoca fallos después de que lo haya hecho virtual, es probable que tenga un error como este en algún lugar allí y lo tuvo allí para empezar porque, como dije, hacer que el destructor sea virtual no puede romper nada por sí solo.


Puede romper algunas pruebas si alguien que derivó de una clase base cambió la política de propiedad de los recursos de la clase:

struct A { int * data; // does not take ownership of data A(int* d) : data(d) {} ~A() { } }; struct B : public A // takes ownership of data { B(int * d) : A (d) {} ~B() { delete data; } };

Y uso:

int * t = new int(8); { A* a = new B(t); delete a; } cout << *t << endl;

Aquí, hacer que el destructor de la A sea virtual causará UB. Sin embargo, no creo que ese uso pueda considerarse una buena práctica.


Sé exactamente de un caso donde un tipo es

  • utilizado como clase base
  • tiene funciones virtuales
  • El destructor no es virtual.
  • Haciendo que el destructor virtual rompa cosas.

y eso es cuando el objeto se ajusta a un ABI externo. Todas las interfaces COM en Windows cumplen con los cuatro criterios. Este no es un comportamiento indefinido, sino garantías específicas de implementación no portátiles relacionadas con la maquinaria de despacho virtual.

Independientemente de su sistema operativo, se reduce a "La regla de una definición". No puede modificar un tipo a menos que vuelva a compilar cada fragmento de código que lo usa contra la nueva definición.


Una cosa que podría cambiar es el tipo de diseño de la clase. Agregar un destructor virtual puede cambiar una clase de ser un tipo de diseño estándar a una clase de diseño no estándar. Entonces, cualquier cosa en la que confíe es que la clase es un tipo POD o de diseño estándar, por ejemplo,

  • clases memcpy o memmve alrededor
  • pasar a funciones C

será UB.


Usted puede "con seguridad" agregar virtual al destructor.

Puede corregir el comportamiento indefinido (UB) si se llama el equivalente de delete base , y luego llamar a los destructores correctos. Si el destructor de la subclase tiene errores, entonces cambia UB por otro error.