析构函数可以递归吗?

Cub*_*bbi 50 c++ destructor standards-compliance

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

我的推理:虽然两次调用析构函数是未定义的行为,但按照12.4/14,它的确如此:

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

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

执行body [...]后,类X的析构函数调用X的直接成员的析构函数,X的直接基类的析构函数[...]

这意味着在从析构函数的递归调用返回之后,将调用所有成员和基类析构函数,并在返回到上一级递归时再次调用它们将是UB.因此,没有基数且只有POD成员的类可以具有不带UB的递归析构函数.我对吗?

Jam*_*lis 60

答案是否定的,因为§3.8/ 1中"生命周期"的定义:

类型对象的生命周期在以下情况T结束:

- 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用将启动,或者

- 重用或释放对象占用的存储空间.

一旦调用析构函数(第一次),对象的生命周期就结束了.因此,如果从析构函数中调用对象的析构函数,则行为未定义,符合§12.4/ 6:

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

  • 噢,没有仔细检查*生命周期*的定义.这听起来很奇怪,因为这意味着在正常调用析构函数期间,我正在访问其生命周期结束的对象的成员.但如果它这样说,那一定是真的. (4认同)
  • @Jacob:如果你想要当前的官方标准,那么[你必须购买](http://stackoverflow.com/questions/81656/where-do-i-find-the-current-c-or-c-标准的文档/ 83763#83763).如果你想要即将到来的C++标准的最终委员会草案(这不是正式的或最终的或批准的,并且有很多很多不同之处),你可以找到[在WG21网站上](http://www.open-std.组织/ JTC1/SC22/WG21 /文档/文件/ 2010 /); 它的文件号是N3092(靠近底部). (2认同)
  • +1:清楚地回答一个奇怪的问题 (2认同)

And*_*rey 9

好的,我们知道行为没有定义.但是,让我们进入真正发生的事情的小旅程.我使用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;
}
Run Code Online (Sandbox Code Playgroud)

让我们运行它并在析构函数中设置一个断点,让递归的奇迹发生.

这是堆栈跟踪:

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

那是什么scalar deleting destructor?这是编译器在delete和我们的实际代码之间插入的东西.析构函数本身只是一种方法,没有什么特别之处.它并没有真正释放内存.它在内部释放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 
Run Code Online (Sandbox Code Playgroud)

在做我们的递归时,我们被困在地址上01341586,而内存实际上只在地址处释放01341597.

结论:在VS 2008中,由于析构函数只是一个方法,并且所有内存释放代码都被注入到中间函数(scalar deleting destructor)中,因此可以安全地递归调用析构函数.但IMO仍然不是个好主意.

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

  • 测试某事物的输出并不表示任何代码的合法性.你得到的,充其量是某个编译器对某些代码的某些行为,但这与C++作为一种语言没有任何关系. (10认同)
  • 拜托了伙计们.这种实验精神和挖掘编译器实际做的事情是值得钦佩和鼓励,而不是批评.他显然不会在现实生活中推荐这一点,而且人们知道C++ - > ASM是如何__ good thing_ (6认同)
  • 这仍然是远程安全的:即使析构函数是"只是"一个成员函数,每次返回时都会调用任何基类和成员变量的析构函数(这很容易测试). (4认同)
  • 哦,不,在某些情况下,没有一个答案"解释"UB"安全".UB不安全,期间.编译器不需要生成行为合理的代码.即使您现在看到此行为,编译器也不需要明天发出相同的机器代码.我最近问了这两个问题:http://stackoverflow.com/questions/3053904/和http://stackoverflow.com/questions/3059917/,答案清楚地说明你可以不遗余力地"证明"UB"在这种情况下是安全的",但你的"证明"无论如何都会比没有任何价值. (2认同)
  • @Andrey:问题是你还没有真正证明VS2008是这样实现的.您所展示的就是*在您编译时,在特定硬件上,使用您的特定示例代码,它生成了此代码.无法保证相同的编译器会再次执行相同的操作.特别是,编译器可以应用完全不相关但安全的优化,这恰好会破坏依赖于未定义行为的代码.这是一个重要的警告.检查编译器实际执行的操作很有意思,但它不会为您提供可依赖的信息. (2认同)

Jay*_*Jay 5

它回到编译器对对象生命周期的定义.就像在,什么时候内存真的被解除分配.我认为直到析构函数完成后才能进行,因为析构函数可以访问对象的数据.因此,我希望递归调用析构函数.

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

也就是说,如果你真的想以递归的方式调用你的析构函数,这不仅仅是一个假设的问题,为什么不只是将析构函数的整个主体撕成另一个函数,让析构函数调用它,然后让它自己调用呢?那应该是安全的.

  • +1表示符合标准的替代方案. (4认同)