【问题标题】:Is throwing an exception from destructor safe for the vtable?从析构函数中抛出异常对于 vtable 是否安全?
【发布时间】:2016-03-04 14:17:57
【问题描述】:

请考虑以下示例:

#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 中运行这个 sn-p 并到达 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?

【问题讨论】:

  • 抛出析构函数很奇怪,尽可能避免。
  • 抛出析构函数比怪异更糟糕。它们实际上是quite dangerous,几乎在所有情况下都应避免使用。
  • 为什么要中止对象的销毁?你知道离开作用域意味着破坏本地对象吗?那么您是否希望编译器向上跳堆栈以避免破坏您的对象?你有没有注意到,清理资源的函数通常总是成功的——并且返回 void。

标签: c++ exception-handling


【解决方案1】:

我一直认为(天真)当程序进入这种情况时,进入 catch 部分,然后 b 点的对象 B 将是完整的,因为异常将“取消”是很合乎逻辑的(如果已编程安全)析构函数的效果。

这不是真的。标准说:

初始化或销毁因异常终止的任何存储持续时间的对象将 为其所有完全构造的子对象(不包括 a 的变体成员)执行析构函数 union-like 类),即对于主要构造函数(12.6.2)已完成执行的子对象 并且析构函数还没有开始执行。

(在 N4140 中为 15.2/2)

而且,可能更重要的是:

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

—如果 T 是具有非平凡析构函数的类类型 (12.4),则析构函数调用开始

(在 N4140 中为 3.8/1.3)

因为b 的每个成员和基础都是完全构造的,并且在输入时没有析构函数,但它们将被视为已销毁。 所以在你的catch 块中,b 指向的整个对象已经死了。

这背后的原因可能是禁止“半破坏”的对象,因为不清楚不能被破坏的对象的状态应该是什么。例如,如果只有一些成员已经被销毁了怎么办?

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

我们可以从上面的引用中得到一个很好的规则:当一个对象的构造函数完成没有抛出时,它就开始存在,一旦它的析构函数它就不再存在了开始执行,不管它如何退出。(这在标准的其他地方更清楚地重申了。这有例外,不要关心它们。)

总之:

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

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

【讨论】:

    【解决方案2】:

    当程序进入这个 情况下,进入 catch 部分,然后是 b 点的对象 B 完好无损,因为异常将具有非常合乎逻辑的 “取消”(如果编程安全)析构函数的效果。

    没有。 lifetime of an object ends when its destructor starts

    你不能取消析构函数。

    正如其他人所说,在 C++ 中抛出析构函数很奇怪,你想避免它们except for special cases

    【讨论】:

    • 看来我有一个特殊情况。在我的真实程序中,B 对象是一个 V4L2 帧包装器,在它的析构函数中,我尝试将其排队返回给驱动程序,如果失败,我希望将销毁包装器推迟到下一次尝试。
    • @krokoziabla:在这种情况下,我建议您需要一个包含指向感兴趣对象的指针的包装器,而不是直接清理对象,而是将指针移动到等待清理的对象队列。然后可以在能够更好地处理由此引发的异常的上下文中处理队列中的对象。 C++ 几乎要求以析构函数不可能失败的方式定义类型,但如果析构函数不负责实际执行清理,而只是安排它完成......
    • ...那么构造函数满足其义务的能力将不依赖于对象实际执行所需清理的能力。
    • 是的,我有一个关于为此使用垃圾收集器的想法,但是析构函数将帧排队的解决方案对我来说似乎很优雅,因为析构函数的工作与构造函数的工作完全相反让他们出队。好吧,生活就是生活。
    • @krokoziabla 听起来你的班级有两个不同的职责。将这些职责分开,这会变得更加简单。
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2010-11-14
      • 1970-01-01
      • 2022-01-16
      • 2015-08-26
      • 2013-04-03
      • 1970-01-01
      • 2014-08-02
      相关资源
      最近更新 更多