正在从vtable的析构函数安全抛出异常

Is throwing an exception from destructor safe for the vtable?

本文关键字:析构函数 安全 抛出异常 vtable      更新时间:2023-10-16

请考虑以下示例:

#include <csignal>
class A
{
public:
    virtual ~A() {}
    virtual void foo() = 0;
};
class B : public A
{
public:
    virtual ~B() { throw 5; } 
    virtual void foo() {}
};
int main(int, char * [])
{
    A * b = new B();
    try
    {
        delete b;
    }
    catch ( ... )
    {
        raise(SIGTRAP);
    }
    return 0;
}

我一直认为(天真的我),当程序在这种情况下进入catch部分时,b指向的对象B将完好无损,因为异常将"取消"(如果安全编程)析构函数的影响是非常合乎逻辑的。但是,当我尝试在gdb中运行这个片段并到达catch部分的断点时,我看到B对象不见了,只剩下A基对象,因为vtable看起来像这样:

(gdb) i vtbl b
vtable for 'A' @ 0x400cf0 (subobject @ 0x603010):
[0]: 0x0
[1]: 0x0
[2]: 0x4008e0 <__cxa_pure_virtual@plt>

我的问题是:如果我非常想从析构函数抛出异常,有没有办法避免vtable的(半)破坏?

我一直认为(天真的我),当程序在这种情况下进入catch部分时,B点处的对象B将完好无损,因为异常将具有";取消";(如果编程安全的话)析构函数的效果。

这不是真的。标准上写着:

初始化或销毁因异常而终止的任何存储持续时间的对象将为其所有完全构造的子对象执行析构函数(不包括类并集),也就是说,对于主构造函数(12.6.2)已经完成执行的子对象并且析构函数尚未开始执行。

(在N4140中为15.2/2)

也许更重要的是:

T类型对象的寿命在以下情况下结束:

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

(N4140中为3.8/1.3)

由于b的每一个成员和基都是完全构造的,并且它们的析构函数都不在其中,但它们将被视为已被破坏因此,在catch块中,b指向的整个对象已经死了

这背后的理性可能是令人生畏的"半破坏的";对象,因为不清楚不能销毁的对象的状态应该是什么。例如,如果只有一些成员已经销毁了呢?

甚至标准本身也建议不要让异常离开析构函数。正如我之前在评论中所写的,抛出析构函数是很奇怪的。

我们可以从上面的引用中得到一个很好的规则:当对象的构造函数完成而不抛出时,对象就开始存在,而当其析构函数开始执行时,它就停止存在,无论它如何退出(这在标准的其他地方得到了更明确的重申。这也有例外,不要在意。)

所以总结一下:

如果我非常想从析构函数抛出异常,有没有办法避免vtable的(半)破坏?

没有。一旦你进入析构函数,你的对象就完成了。

当程序进入case,进入catch部分,然后B点所在的对象B完整,因为异常将具有"取消"(如果安全编程)析构函数的效果。

没有。当对象的析构函数启动时,对象的生存期结束

不能取消析构函数。

正如其他人所说,在C++中抛出析构函数是很奇怪的,除了特殊情况外,你需要避免它们。

就实例而言,从析构函数抛出是定义良好且安全的。您开始遇到问题的地方是数组(因为它无法完成删除数组的操作,而且您无法将其取回)和catch子句(最终可能调用terminate)。如果析构函数抛出,编写异常安全代码也很困难(我认为这实际上是不可能的,但还没有准备好从内存中声明这一点)。

不过,我用过抛出析构函数来做一些事情。例如,我使用的API可能会返回错误代码并分配错误blob。我写了一个小范围保护的东西,它会分发引用来放入数据,并检查析构函数中的错误条件。如果它看到一个异常,就会将其转换为异常并抛出

这样的结构在技术上是安全的,但你有点想避免它,直到你知道自己在做什么。您必须明确这些东西不能存储在向量或数组中,并且可能会使异常安全代码变得不安全。主要的问题是,几乎每个人都希望所有的析构函数都是另一个。