析构函数可以递归吗?

发布于 2024-09-06 05:12:47 字数 1065 浏览 6 评论 0原文

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

#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,它的确切含义是这样的:

如果以下情况,则行为未定义 为对象调用析构函数 其生命已结束

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

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

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

Is this program well-defined, and if not, why exactly?

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

My reasoning: although invoking a destructor twice is undefined behavior, per 12.4/14, what it says exactly is this:

the behavior is undefined if the
destructor is invoked for an object
whose lifetime has ended

Which does not seem to prohibit recursive calls. While the destructor for an object is executing, the object's lifetime has not yet ended, thus it's not UB to invoke the destructor again. On the other hand, 12.4/6 says:

After executing the body [...] a
destructor for class X calls the
destructors for X's direct members,
the destructors for X's direct base
classes [...]

which means that after the return from a recursive invocation of a destructor, all member and base class destructors will have been called, and calling them again when returning to the previous level of recursion would be UB. Therefore, a class with no base and only POD members can have a recursive destructor without UB. Am I right?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

七月上 2024-09-13 05:12:48

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

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

—如果 T 是具有重要析构函数的类类型 (12.4),则析构函数调用开始,或者

——对象占用的存储被重用或释放。

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

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

The answer is no, because of the definition of "lifetime" in §3.8/1:

The lifetime of an object of type T ends when:

— if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or

— the storage which the object occupies is reused or released.

As soon as the destructor is called (the first time), the lifetime of the object has ended. Thus, if you call the destructor for the object from within the destructor, the behavior is undefined, per §12.4/6:

the behavior is undefined if the destructor is invoked for an object whose lifetime has ended

川水往事 2024-09-13 05:12:48

好的,我们知道行为是没有定义的。但让我们来简单了解一下到底发生了什么。我使用 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;
}

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

这是堆栈跟踪:

替代文字

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

让我们转到标量删除析构函数并看一下反汇编:

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 

在进行递归时,我们被困在地址01341586处,并且内存实际上仅在地址处被释放01341597

结论:在VS 2008中,由于析构函数只是一个方法,并且所有内存释放代码都注入到中间函数(标量删除析构函数)中,因此递归调用析构函数是安全的。但在我看来,这仍然不是一个好主意。

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

Okay, we understood that behavior is not defined. But let's do small journey into what really happends. I use VS 2008.

Here is my 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;
}

Let's run it and set a breakpoint inside destructor and let the miracle of recursion happen.

Here is stack trace:

alt text

What is that scalar deleting destructor? It is something that compiler inserts between delete and our actual code. Destructor itself is just a method, there is nothing special about it. It doesn't really release the memory. It is released somewhere inside that scalar deleting destructor.

Let's go to scalar deleting destructor and take a look at the disassembly:

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 

while doing our recursion we are stuck at address 01341586, and memory is actually released only at address 01341597.

Conclusion: In VS 2008, since destructor is just a method and all memory release code are injected into middle function (scalar deleting destructor) it is safe to call destructor recursively. But still it is not good idea, IMO.

Edit: Ok, ok. The only idea of this answer was to take a look at what is going on when you call destructor recursively. But don't do it, it is not safe generally.

于我来说 2024-09-13 05:12:48

这又回到了编译器对对象生命周期的定义。比如,内存什么时候真正被释放。我认为直到析构函数完成之后才能完成,因为析构函数可以访问对象的数据。因此,我希望对析构函数的递归调用能够起作用。

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

也就是说,如果您确实想递归地调用析构函数并且这不仅仅是一个假设的问题,为什么不将析构函数的整个主体分解为另一个函数,让析构函数调用它,然后让它递归地调用自身呢?那应该是安全的。

It comes back to the compiler's definition of the lifetime of an object. As in, when is the memory really de-allocated. I would think it could not be until after the destructor has completed, as the destructor has access to the object's data. Therefore, I would expect recursive calls to the destructor to work.

But ... there are surely many ways to implement a destructor and the freeing of memory. Even if it worked as I wanted on the compiler I'm using today, I would be very cautious about relying on such behavior. There are lots of things where the documentation says it won't work or the results are unpredictable that in fact work just fine if you understand what is really happening inside. But it's bad practice to rely on them unless you really have to, because if the specs say that this doesn't work, then even if it really does work, you have no assurance that it will continue to work in the next version of the compiler.

That said, if you really want to call your destructor recursively and this isn't just a hypothetical question, why not just rip the entire body of the destructor into another function, let the destructor call that, and then let that call itself recursively? That should be safe.

温柔戏命师 2024-09-13 05:12:48

是的,听起来不错。我认为一旦析构函数完成调用,内存将被转储回可分配池,允许在其上写入内容,从而可能导致后续析构函数调用出现问题(“this”指针将无效)。

但是,如果析构函数在递归循环展开之前没有完成......理论上应该没问题。

有趣的问题:)

Yeah, that sounds about right. I would think once the destructor is finished calling, the memory would be dumped back into the allocatable pool, allowing something to write over it, thus potentially causing issues with follow-up destructor calls (the 'this' pointer would be invalid).

However, if the destructor doesn't finish until the recursive loop is unwound.. it should theoretically be fine.

Interesting question :)

撞了怀 2024-09-13 05:12:48

为什么有人想以这种方式递归调用析构函数?一旦调用了析构函数,它就应该销毁该对象。如果您再次调用它,您将尝试启动对已经部分销毁的对象的销毁,而实际上您同时实际上仍在销毁它。

所有示例都有某种递减/增量结束条件,
本质上是在调用中倒计时,这暗示着包含与其自身类型相同的成员的嵌套类的某种失败实现。

对于这样一个嵌套的俄罗斯套娃类,递归地调用成员的析构函数,即析构函数调用成员A的析构函数,成员A又调用其自己的成员A的析构函数,成员A又调用析构函数...等等非常好,并且完全按照人们的预期工作。这是析构函数的递归使用,但它并不是递归地调用析构函数本身,这是疯狂的,而且几乎没有意义。

Why would anyone ever want to call the destructor recursively in this way ? Once you have called the destructor, it should destroy the object. If you call it again, you would be trying to initiate the destruction of an already partly destroyed object when you were still actually part way through actually destroying it at the same time.

All of the examples have some sort of decremental / incremental end condition,
to essentially count down in calls, which is suggestive of some sort of failed implementation of a nested classs which contains members of the same type as itself.

For such a nested matryoshka class, calling the destructor on the members, recursively, ie the destructor calls the destructor on member A, which in turn calls the destructor on its own member A, which in turn calls the detructor ... and so on is perfectly fine and works exactly as one might expect. This is a recursive use of the destructor, but it is not recursively calling the destructor on itself which is insane, and would make almost no sense.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文