析构函数是在 C++ 抛出后调用的吗?

Are destructors called after a throw in C++?

提问人:Luchian Grigore 提问时间:11/29/2011 最后编辑:Luchian Grigore 更新时间:6/19/2019 访问量:27284

问:

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

C++ 异常 try-catch raii

评论

19赞 Jon 11/29/2011
当然可以。RAII是C++中最重要的习语之一,它依赖于此。
0赞 Kerrek SB 11/29/2011
是的,这就是异常处理的全部意义所在。
1赞 Philippe Carphin 9/17/2020
@Jon & Kerrek SB,如果未捕获异常,则不能保证发生堆栈展开,这是实现定义的:请参阅下面的NPE答案,最后一部分是标准中的引用。

答:

87赞 NPE 11/29/2011 #1

是的,它是有保证的(前提是捕获到异常),直到调用析构函数的顺序

C++11 15.2 构造函数和析构函数 [except.ctor]

1 当控制从抛出表达式传递到处理程序时,将对所有 自输入 try 块以来构造的自动对象。这 自动对象以与完成的相反顺序销毁 他们的建筑。

此外,如果在对象构造过程中抛出异常,则保证部分构造对象的子对象被正确销毁:

2 任何存储持续时间的对象,其初始化或 销毁被异常终止时将有析构函数 针对其所有完全构造的子对象执行(不包括 类联合类的变体成员),即用于 主构造函数 (12.6.2) 已完成执行,并且 析构函数尚未开始执行。同样,如果 对象的非委托构造函数已完成执行,并且 委托该对象的构造函数退出,并出现异常, 对象的析构函数将被调用。如果对象是在 new-expression,匹配的释放函数(3.7.4.2、5.3.4、 12.5) (如果有)以释放对象占用的存储空间。

这整个过程被称为“堆垛放卷”:

3 自动构造对象调用析构函数的过程 在从 try 块到 throw-expression 的路径上称为“堆栈” 放松。如果在堆栈展开期间调用的析构函数退出 异常 std::terminate 被调用 (15.5.1)。

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

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

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

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

评论

7赞 Matthieu M. 11/29/2011
注意:关于被中断的对象的构造。对象本身不会被销毁(它实际上从未存在过),可以保证的是,到目前为止已经完全构建的子部分(基类、属性)将以相反的顺序被销毁。
0赞 Cheers and hth. - Alf 11/29/2011
添加了有关未捕获异常的堆栈展开(或不展开)的信息。
0赞 Gregor Hartl Watters 3/7/2023
如果异常未捕获怎么办?
10赞 user405725 11/29/2011 #2

是的,析构函数保证在堆栈展开时被调用,包括由于抛出异常而展开。只有少数技巧是您必须记住的例外:

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

评论

9赞 DevSolar 11/29/2011
3) 析构函数永远不应该抛出异常,因为没有办法充分处理它们。
4赞 Cheers and hth. - Alf 11/29/2011
@DevSolar确实存在一些反例。
1赞 DevSolar 11/29/2011
@AlfP.Steinbach:在堆栈展开期间抛出的任何析构函数(由于抛出另一个异常)都会对您的进程造成影响。我很想看看反例......terminate()
2赞 Cheers and hth. - Alf 11/30/2011
@DevSolar:你(故意的?)不清楚你想要什么反例。但是关于第一个声明,即“析构函数永远不应该抛出异常”,一个并不罕见的反例是一个表示函数结果的对象,如果调用方代码没有检查它是否表示失败,则从其析构函数抛出。另一个例子是事务保护对象,它从其析构函数中抛出,除非嵌入它的代码已经成功(例如,转移某物的所有权)并调用了它的方法。release
5赞 DevSolar 11/30/2011
@AlfP.Steinbach:“使用代码可以围绕它进行设计。但是,经验法则是,您的对象可能会在您没有预见到的上下文中使用,并且无法主动满足需求。例如,如果你把抛出析构函数的对象放在一个 STL 向量中,并且该向量由于一些不相关的异常堆栈展开而被销毁,它将调用你的对象的析构函数,你的应用程序就会“噗”一声。当然,我可以把手榴弹上的销钉从手榴弹上拔下来,小心地处理一会儿,然后把销钉放回去。但我永远不应该那样做......
3赞 user9599745 6/19/2019 #3

如果投掷被捕获,则通常 cpp 操作将继续。这包括析构函数和堆栈弹出。但是,如果未捕获异常,则不能保证堆栈弹出。

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

例:

#include <Jav/report.h>

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

评论

1赞 davernator 7/26/2019
我给了这个加分。它不仅没有被捕获,而且 try 块中自动对象的析构函数(很容易插入一个)也不会被调用 (g++ 7.4.0/clang++ 6.0.0 ubuntu), -std=c++[11|14|17]。将 noexcept 与主声明一起使用似乎没有帮助。我还尝试了相关的手册页选项。我可以收到关于在接球块中裸抛的警告,但在尝试块中却不能。如果我忽略了什么,请启发。
0赞 3/13/2020
@davernator 谢谢你的加一。我希望你没有忽视任何事情,因为这高于我的工资等级。即使是现在。和平出来。