Un destructeur peut-il être récursif?
Ce programme est-il bien défini, et sinon, pourquoi exactement?
#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;
}
Mon raisonnement: bien que invoquer un destructeur deux fois soit un comportement indéfini , par 12.4 / 14, ce qu'il dit exactement est ceci:
Le comportement est indéfini si le le destructeur est appelé pour un objet dont la vie est terminée
, Qui ne semble pas interdire les appels récursifs. Alors que le destructeur d'un objet est en cours d'exécution, la durée de vie de l'objet n'est pas encore terminée, donc ce n'est pas le cas UB pour appeler à nouveau le destructeur. D'autre part, 12.4 / 6 dit:
Après l'exécution du corps [...] un destructeur pour la classe X appelle le destructeurs pour les membres directs de X, les destructeurs pour la base directe de X classe [...]
Ce qui signifie qu'après le retour d'une invocation récursive d'un destructeur, tous les destructeurs de membre et de classe de base auront été appelés, et les appeler à nouveau lors du retour au niveau de récursivité précédent serait UB. Par conséquent, une classe sans base et seuls les membres de POD peuvent avoir un destructeur récursif sans UB. Suis-je le droit?
5 réponses
La réponse est non, en raison de la définition de" durée de vie " au §3.8 / 1:
La durée de vie d'un objet de type
T
se termine lorsque:- Si
T
est un type de classe avec un destructeur non trivial (12.4) , l'appel destructor démarre, ou- le stockage que l'objet occupe est réutilisé ou libéré.
Dès Que le destructeur est appelé (la première fois), la durée de vie de l'objet est terminé. Ainsi, si vous appelez le destructeur pour l'objet de dans le destructeur, le comportement est indéfini, par §12.4 / 6:
Le comportement n'est pas défini si le destructeur est appelé pour un objet dont la durée de vie est terminée
D'accord, nous avons compris que le comportement n'est pas défini. Mais faisons petit voyage dans ce qui se passe vraiment. J'utilise VS 2008.
Voici mon code:
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;
}
Exécutons-le et définissons un point d'arrêt à l'intérieur de destructor et laissons le miracle de la récursivité se produire.
Voici trace de pile:
Le texte d'Alt http://img638.imageshack.us/img638/8508/dest.png
Qu'est-Ce que c' scalar deleting destructor
? C'est quelque chose que le compilateur insère entre delete et notre code réel. Destructeur lui-même est juste une méthode, il n'y a rien de spécial à ce sujet. Il ne libère pas vraiment la mémoire. Il est libéré quelque part à l'intérieur de scalar deleting destructor
.
Allons à scalar deleting destructor
et regardons le démontage:
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
En faisant notre récursivité, nous sommes bloqués à l'adresse 01341586
, et la mémoire n'est réellement libérée qu'à l'adresse 01341597
.
Conclusion: dans VS 2008, puisque destructor est juste une méthode et que tout le code de libération de mémoire est injecté dans la fonction du milieu (scalar deleting destructor
), Il est sûr d'appeler destructeur récursivement. Mais ce n'est toujours pas une bonne idée, IMO.
Modifier : Ok, ok. La seule idée de cette réponse était de jeter un oeil à ce qui se passe lorsque vous appelez destructor récursivement. Mais ne le faites pas, ce n'est généralement pas sûr.
Il revient à la définition du compilateur de la durée de vie d'un objet. Comme dans, quand la mémoire est-elle vraiment désallouée. Je pense que cela ne pourrait pas être avant que le destructeur ne soit terminé, car le destructeur a accès aux données de l'objet. Par conséquent, je m'attendrais à ce que les appels récursifs au destructeur fonctionnent.
Mais ... il y a sûrement plusieurs façons de mettre en œuvre un destructeur et la libération de la mémoire. Même si cela fonctionnait comme je le voulais sur le compilateur que j'utilise aujourd'hui, je serais très prudent de compter sur un tel comportement. Il y a beaucoup de choses où la documentation dit que cela ne fonctionnera pas ou que les résultats sont imprévisibles, ce qui fonctionne très bien si vous comprenez ce qui se passe réellement à l'intérieur. Mais c'est une mauvaise pratique de compter sur eux, sauf si vous devez vraiment, parce que si les spécifications dire que cela ne fonctionne pas, alors même si il fonctionne vraiment, vous n'avez pas l'assurance qu'il continuera à travailler sur la prochaine version du compilateur.
Cela dit, si vous voulez vraiment pour appeler votre destructeur récursivement et ce n'est pas seulement une question hypothétique, pourquoi ne pas simplement déchirer le corps entier du destructeur dans une autre Fonction, Laisser le destructeur appeler cela, puis laisser cela s'appeler récursivement? Qui devrait être en sécurité.
Ouais, ça a l'air juste. Je pense qu'une fois que le destructeur aura fini d'appeler, la mémoire sera renvoyée dans le pool allouable, permettant à quelque chose d'écrire dessus, causant ainsi potentiellement des problèmes avec les appels de destructeur de suivi (le pointeur' this ' serait invalide).
Cependant, si le destructeur ne se termine pas tant que la boucle récursive n'est pas déroulée.. ça devrait théoriquement aller.
Question Intéressante :)
Pourquoi quelqu'un voudrait appeler récursivement le destructeur de cette façon ? Une fois que vous avez appelé le destructeur, il devrait détruire l'objet. Si vous l'appelez à nouveau, vous essayeriez d'initier la destruction d'un objet déjà partiellement détruit alors que vous étiez encore en train de le détruire en même temps.
Tous les exemples ont une sorte de condition de fin décrémentale / incrémentale, compte à rebours dans les appels, ce qui suggère une sorte d'implémentation échouée d'une classe imbriquée qui contient des membres du même type que lui-même.
Pour une telle classe matryoshka imbriquée, appeler le destructeur sur les membres, récursivement, c'est-à-dire que le destructeur appelle le destructeur sur le membre a, qui à son tour appelle le destructeur sur son propre membre A, qui à son tour appelle le détructor ... et ainsi de suite est parfaitement bien et fonctionne exactement comme on pourrait s'y attendre. C'est une utilisation récursive du destructeur, mais c'est Pas récursivement appeler le destructeur sur lui-même qui est fou, et n'aurait presque aucun sens.