在C++中,析构函数在抛出异常后是否被调用?

65

我运行了一个样例程序,确实会调用栈分配对象的析构函数,但这是否由标准保证呢?


19
当然可以。RAII是C++中最重要的惯用语之一,它依赖于这个原理。 - Jon
是的,这就是异常处理的全部意义。 - Kerrek SB
1
@Jon & Kerrek SB,如果异常没有被捕获,堆栈展开不一定会发生,这是实现定义的:请参见下面NPE的答案,最后一部分是引用标准,说明了这一点。 - Philippe Carphin
3个回答

87
是的,它是有保障的(前提是异常被捕获),包括析构函数被调用的顺序:

C++11 15.2 构造函数和析构函数 [except.ctor]:
1 当控制权从 throw-expression 转移到 handler 时,自 try 块进入以来构造的所有自动对象都会调用析构函数。这些自动对象按完成构造的相反顺序销毁。

此外,如果异常在对象构造期间抛出,则部分构造对象的子对象将得到正确的销毁:

2 任何初始化或销毁由异常终止的存储期对象都将对其所有完全构造的子对象(不包括联合类的变体成员)执行析构函数,即已经执行完主要构造函数(12.6.2)而尚未开始执行析构函数的子对象。同样地,如果对象的非委托构造函数已经执行完毕,并且该对象的委托构造函数因异常退出,则将调用对象的析构函数。如果对象是在 new-expression 中分配的,则调用匹配的释放函数(3.7.4.2、5.3.4、12.5)(如果有的话)来释放对象占用的存储空间。

整个过程称为“堆栈展开”:

3 在从 try 块到 throw 表达式的路径上构造的自动对象的调用析构函数的过程称为 “堆栈展开” 。如果在堆栈展开期间调用的析构函数退出并引发异常,则会调用 std::terminate (15.5.1)。

堆栈展开是广泛使用的 资源获取即初始化 (RAII) 技术的基础。

请注意,如果未捕获异常,则不一定执行堆栈展开。在这种情况下,是否执行堆栈展开取决于实现。但无论是否执行堆栈展开,在这种情况下,你都保证会最终调用 std::terminate

C++11 15.5.1 函数 std::terminate() [except.terminate]

2 … 在找不到匹配处理程序的情况下,实现定义在调用 std::terminate() 之前是否展开堆栈。


7
注意:关于被中断的对象的构建。对象本身并没有被销毁(它实际上从未存在),保证的是到目前为止已经完全构建的子部分(基类、属性)将按相反的顺序被销毁。 - Matthieu M.
增加了有关未捕获异常的堆栈展开(或不展开)的信息。 - Cheers and hth. - Alf
如果异常没有被捕获会怎么样? - Gregor Hartl Watters

10

是的,在堆栈展开时,包括由于抛出异常而导致的展开,析构函数得到保证会被调用。有几个关于异常需要注意的技巧:

  • 如果在类的构造函数中抛出异常,则不会调用该类的析构函数。
  • 如果在构造初始化列表catch块中捕获异常,则异常会自动重新抛出。

9
析构函数绝对不应该抛出异常,因为没有适当的方式来处理它们。 - DevSolar
4
@DevSolar 存在一些反例。 - Cheers and hth. - Alf
1
@AlfP.Steinbach:在堆栈展开期间抛出的任何析构函数(由于抛出了另一个异常)都将terminate()您的进程。我很想看到反例... - DevSolar
2
@DevSolar:你故意不说你想要反例的东西吗?但关于第一个主张,“析构函数永远不应该抛出异常”,一个并不完全罕见的反例是代表功能结果的对象,在其析构函数中抛出异常,如果调用者代码没有检查它是否表示失败。另一个例子是事务保护对象,在其析构函数中抛出异常,除非嵌入其中的代码已经成功地完成了它的努力(例如传输所有权),并调用了它的“释放”方法。 - Cheers and hth. - Alf
1
问题是,正如您所指出的那样,在析构函数中抛出异常如果存在未处理的异常,可以非常致命(不是堆栈展开,因为您可以在析构函数中执行 try)。Visual C++ 从未实现标准的检查函数,而且该函数也不太适当。因此,这有点棘手,但并非完全无法解决,因为使用代码可以设计解决方案。 - Cheers and hth. - Alf
5
@AlfP.Steinbach: “Usage code can be designed around it.” - 我同意这种说法。然而,经验法则是你的对象可能会在你未预见到的情况下使用,并且无法主动地为其提供支持。例如,如果你将析构函数抛出异常的对象放入STL向量中,而该向量由于某个不相关的异常堆栈解旋而被销毁,它将调用你对象的析构函数,从而导致你的应用程序“崩溃”。当然,我可以拔掉手雷的保险销,在一段时间内小心翼翼地处理它,然后再把保险销插回去。但我永远不应该那样做… - DevSolar

3

如果一个抛出异常被捕获,那么通常cpp操作会继续执行。这包括析构函数和堆栈弹出。但是如果异常未被捕获,则不能保证堆栈弹出。

此外,我的移动编译器无法捕获裸抛或空抛。

示例:

#include <Jav/report.h>

int main()
{
 try { throw; }
 catch(...) { rep("I bet this is not caught"); }
 }

1
我给这个加了一个赞。不仅没有被捕获,而且在try块中的自动对象的析构函数(很容易插入一个)也没有被调用(g++ 7.4.0 / clang++ 6.0.0 ubuntu),-std=c++[11|14|17]。使用noexcept声明主函数似乎没有帮助。我还尝试了相关的man页面选项。我可以得到一个警告,对于catch块中的裸抛出,但是在try块中却没有。如果我忽略了什么,请指教。 - davernator
@davernator 感谢你的点赞。希望你没有忽略任何事情,因为那超出了我的薪资范围。即使现在也是如此。祝好。 - user9599745

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接