c++ - serie - ¿Puede un destructor ser recursivo?
serie fibonacci c++ (5)
¿Este programa está bien definido, y si no, por qué exactamente?
#include <iostream>
#include <new>
struct X {
int cnt;
X (int i) : cnt(i) {}
~X() {
std::cout << "destructor called, cnt=" << cnt << std::endl;
if ( cnt-- > 0 )
this->X::~X(); // explicit recursive call to dtor
}
};
int main()
{
char* buf = new char[sizeof(X)];
X* p = new(buf) X(7);
p->X::~X(); // explicit call to dtor
delete[] buf;
}
Mi razonamiento: aunque invocar un destructor dos veces es un comportamiento indefinido , por 12.4 / 14, lo que dice exactamente es esto:
el comportamiento no está definido si se invoca el destructor para un objeto cuya vida útil ha terminado
Lo cual no parece prohibir llamadas recursivas. Mientras se ejecuta el destructor para un objeto, la duración del objeto aún no ha finalizado, por lo tanto, no es UB para invocar el destructor nuevamente. Por otro lado, 12.4 / 6 dice:
Después de ejecutar el cuerpo, [...] un destructor para la clase X llama a los destructores para los miembros directos de X, los destructores para las clases base directas de X [...]
lo que significa que después del retorno de una invocación recursiva de un destructor, todos los miembros y los destructores de la clase base habrán sido llamados, y llamarlos de nuevo cuando regrese al nivel previo de recursión sería UB. Por lo tanto, una clase sin base y solo miembros POD puede tener un destructor recursivo sin UB. ¿Estoy en lo cierto?
¿Por qué alguien querría llamar al destructor recursivamente de esta manera? Una vez que haya llamado al destructor, debería destruir el objeto. Si lo llamas de nuevo, estarías tratando de iniciar la destrucción de un objeto ya parcialmente destruido cuando todavía estabas en la mitad del camino destruyéndolo al mismo tiempo.
Todos los ejemplos tienen algún tipo de condición final decremental / incremental, para esencialmente realizar una cuenta atrás en las llamadas, lo que sugiere algún tipo de implementación fallida de una clase anidada que contiene miembros del mismo tipo que ella.
Para una clase matryoshka anidada, llamar al destructor en los miembros, recursivamente, es decir, el destructor llama al destructor en el miembro A, que a su vez llama al destructor en su propio miembro A, que a su vez llama al detructor ... y así sucesivamente está perfectamente bien y funciona exactamente como uno podría esperar. Este es un uso recursivo del destructor, pero no está llamando recursivamente al destructor sobre sí mismo, que es una locura, y casi no tendría sentido.
De acuerdo, entendimos que el comportamiento no está definido. Pero hagamos un pequeño viaje hacia lo que realmente sucede. Yo uso VS 2008.
Aquí está mi código:
class Test
{
int i;
public:
Test() : i(3) { }
~Test()
{
if (!i)
return;
printf("%d", i);
i--;
Test::~Test();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
delete new Test();
return 0;
}
Vamos a ejecutarlo y establecer un punto de interrupción dentro de destructor y dejar que el milagro de la recursión suceda.
Aquí está el seguimiento de la pila:
texto alternativo http://img638.imageshack.us/img638/8508/dest.png
¿Qué es ese scalar deleting destructor
? Es algo que el compilador inserta entre eliminar y nuestro código real. Destructor en sí mismo es solo un método, no tiene nada de especial. Realmente no libera la memoria. Se libera en algún lugar dentro de ese scalar deleting destructor
.
Vayamos al scalar deleting destructor
y echemos un vistazo al desmontaje:
01341580 mov dword ptr [ebp-8],ecx
01341583 mov ecx,dword ptr [this]
01341586 call Test::~Test (134105Fh)
0134158B mov eax,dword ptr [ebp+8]
0134158E and eax,1
01341591 je Test::`scalar deleting destructor''+3Fh (134159Fh)
01341593 mov eax,dword ptr [this]
01341596 push eax
01341597 call operator delete (1341096h)
0134159C add esp,4
mientras realizamos nuestra recursión, estamos atascados en la dirección 01341586
, y la memoria se libera solo en la dirección 01341597
.
Conclusión: en VS 2008, dado que el destructor es solo un método y todos los códigos de liberación de memoria se inyectan en la función intermedia ( scalar deleting destructor
) es seguro llamar al destructor recursivamente. Pero aún así no es una buena idea, IMO.
Editar : Ok, vale. La única idea de esta respuesta era echar un vistazo a lo que sucede cuando llamas a destructor recursivamente. Pero no lo hagas, no es seguro en general.
La respuesta es no, debido a la definición de "vida" en §3.8 / 1:
La vida útil de un objeto de tipo
T
finaliza cuando:- si
T
es un tipo de clase con un destructor no trivial (12.4), se inicia la llamada al destructor, o- el almacenamiento que ocupa el objeto se reutiliza o libera.
Tan pronto como se llama al destructor (la primera vez), la vida útil del objeto ha finalizado. Por lo tanto, si llama al destructor para el objeto desde el destructor, el comportamiento no está definido, por §12.4 / 6:
el comportamiento no está definido si se invoca el destructor para un objeto cuya vida útil ha terminado
Sí, eso suena bien. Yo pensaría que una vez que el destructor termine de llamar, la memoria se volcaría al conjunto asignable, permitiendo que algo lo escriba, lo que podría causar problemas con las llamadas de seguimiento de destructor (el puntero ''this'' no sería válido).
Sin embargo, si el destructor no termina hasta que se desenrolla el bucle recursivo ... teóricamente debería estar bien.
Interesante pregunta :)
Vuelve a la definición del compilador de la vida útil de un objeto. Como en, ¿cuándo se desasignó realmente la memoria? Pensaría que no podría ser hasta después de que el destructor se haya completado, ya que el destructor tiene acceso a los datos del objeto. Por lo tanto, esperaría que las llamadas recursivas al destructor funcionen.
Pero ... seguramente hay muchas formas de implementar un destructor y la liberación de la memoria. Incluso si funcionó como quería en el compilador que estoy usando hoy, sería muy cauteloso al confiar en ese comportamiento. Hay muchas cosas en las que la documentación dice que no funcionará o los resultados son impredecibles que de hecho funcionan bien si usted comprende lo que realmente sucede dentro. Pero es una mala práctica confiar en ellos a menos que realmente lo necesites, porque si las especificaciones dicen que esto no funciona, incluso si realmente funciona, no tienes la seguridad de que continuará funcionando en la próxima versión del compilador.
Dicho esto, si realmente quieres llamar a tu destructor de forma recursiva y esta no es solo una pregunta hipotética, ¿por qué no simplemente rasgas todo el cuerpo del destructor en otra función, dejas que el destructor lo llame y luego dejes que se llame recursivamente? Eso debería ser seguro.