一个析构函数可以recursion吗?

这个程序是否定义明确,如果不是,为什么?

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

我的推理:虽然调用析构函数两次是未定义的行为 ,每12.4 / 14,它究竟是这样说的:

如果调用析构函数的生命周期结束的对象,行为是未定义的

这似乎不禁止recursion调用。 当一个对象的析构函数正在执行时,该对象的生命周期还没有结束,因此再次调用析构函数并不是UB。 另一方面,12.4 / 6说:

在执行正文后,X类的析构函数调用X的直接成员的析构函数,X的直接基类的析构函数[…]

这意味着在从析构函数的recursion调用返回之后,所有成员和基类析构函数将被调用,并且在返回到先前的recursion级别时再次调用它们将是UB。 因此,没有基础和只有POD成员的类可以有没有UB的recursion析构函数。 我对吗?

答案是否定的,因为在§3.8/ 1中定义了“lifetime”

typesT的对象的生命周期在以下情况下结束:

– 如果T是一个具有非平凡析构函数的类types(12.4),则析构函数调用开始,或者

– 对象占用的存储空间被重用或释放。

一旦调用析构函数(第一次),对象的生命周期就结束了。 因此,如果从析构函数中为对象调用析构函数,则根据§12.4/ 6,行为是不确定的:

如果调用析构函数的生命周期结束的对象,行为是未定义的

好的,我们明白行为没有定义。 但是,让我们做一些真正发生的事情。 我使用VS 2008。

这是我的代码:

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

让我们运行它,并在析构函数内设置一个断点,让recursion的奇迹发生。

这里是堆栈跟踪:

替代文字http://img638.imageshack.us/img638/8508/dest.png

这个scalar deleting destructor什么? 这是编译器在删除和我们的实际代码之间插入的东西。 析构函数本身只是一个方法,没有什么特别的。 它并不真正释放内存。 它被释放在scalar deleting destructor某个地方。

让我们去scalar deleting destructor并看看反汇编:

 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 

在做recursion的时候,我们被困在地址01341586 ,而内存实际上只在地址01341597处被释放。

结论:在VS 2008中,由于析构函数只是一种方法,所有的内存释放代码都被注入到中间函数( scalar deleting destructor )中,recursion调用析构函数是安全的。 但是,IMO还是不好的主意。

编辑 :好的,好的。 这个答案的唯一想法是看看当你调用析构函数recursion时发生了什么。 但是不这样做,一般是不安全的。

它回到编译器的对象生命周期的定义。 就像在什么时候内存真的被解除分配一样。 我认为它不能直到析构函数完成之后,因为析构函数可以访问对象的数据。 因此,我希望recursion调用析构函数来工作。

但是…确实有很多方法来实现析构函数和释放内存。 即使它在我今天使用的编译器上按照我的想法工作,依靠这样的行为我也会非常谨慎。 文档中说有很多东西不能运行,或者结果是不可预测的,事实上,如果你了解内部真正发生的事情,它就可以很好地工作。 但是,除非你真的必须依靠它们,否则这是不好的做法,因为如果规范说这不起作用,那么即使它确实起作用,也不能保证它会继续在下一个版本中继续工作编译器。

也就是说,如果你真的想recursion地调用你的析构函数,这不仅仅是一个假设的问题,为什么不把这个析构函数的全部转换成另一个函数,让析构函数调用它,然后recursion地调用它自己呢? 这应该是安全的。

是的,这听起来是正确的。 我会认为一旦析构函数完成调用,内存将被转储回可分配池,允许写一些东西,从而可能导致后续析构函数调用(“this”指针将无效)的问题。

然而,如果析构函数没有完成,直到recursion循环解开..理论上应该罚款。

有趣的问题:)

为什么会有人想以这种方式recursion调用析构函数? 一旦你调用了析构函数,它应该销毁这个对象。 如果你再次调用它,那么当你实际上同时通过实际摧毁它时,你将会试图破坏一个已经被部分摧毁的对象。

所有的例子都有一些递减的/递增的结束条件,基本上在调用中倒计时,这暗示了一些嵌套类的失败实现,这些嵌套类包含与它自己types相同的成员。

对于这样一个嵌套matryoshka类,在成员上调用析构函数,即析构函数调用成员A的析构函数,然后调用它自己的成员A上的析构函数,然后调用析构函数…等等是完全正常的,完全按照人们的预期工作。 这是析构函数的recursion使用,但它不是recursion地调用自己的析构函数,这是疯狂的,几乎没有意义。

Interesting Posts