析构函数中的 C++ 异常

c++ exception in destructor

本文关键字:异常 C++ 析构函数      更新时间:2023-10-16

从其他线程中,我知道我们不应该在析构函数中抛出异常!但对于下面的例子,它确实有效。这是否意味着我们只能在一个实例的析构函数中抛出异常?我们应该如何理解这个代码示例!

#include <iostream>
using namespace std;
class A {
public:
~A() {
try {
printf("exception in A startn");
throw 30;
printf("exception in A endn");      
}catch(int e) {
printf("catch in A %dn",e);
}
}
};
class B{
public:
~B() {
printf("exception in B startn");
throw 20;
printf("exception in B endn");    
}
};
int main(void) {
try {
A a;
B b;
}catch(int e) {
printf("catch in main %dn",e);
}
return 0;
}

输出为:

exception in B start
exception in A start
catch in A 30
catch in main 20

C++17 之前的最佳实践是不要让异常从析构函数中传播出去。如果析构函数包含throw表达式或调用可能引发的函数,只要捕获并处理引发的异常而不是从析构函数中转义即可。 所以你的A::~A很好。

B::~B的情况下,您的程序在 C++03 中很好,但在 C++11 中则不行。规则是,如果您确实让异常从析构函数中传播出来,并且该析构函数用于通过堆栈展开直接销毁的自动对象,则将调用std::terminate。由于b不会作为堆栈展开的一部分被销毁,因此将捕获从B::~B抛出的异常。但是在 C++11 中,B::~B析构函数将被隐式声明为noexcept,因此,允许异常从中传播出来将无条件地调用std::terminate

要允许在 C++11 中捕获异常,您可以编写

~B() noexcept(false) {
// ...
}

尽管如此,还是会出现一个问题,即在堆栈展开期间可能会调用B::~B---在这种情况下,将调用std::terminate。由于在 C++17 之前,无法判断是否是这种情况,因此建议永远不允许异常从析构函数中传播。遵循这个规则,你会没事的。

在C++17中,可以使用std::uncaught_exceptions()来检测在堆叠放卷过程中物体是否被破坏。但你最好知道你在做什么。

"我们不应该在析构函数中抛出异常"的建议不是绝对的。问题是,当引发异常时,编译器开始展开堆栈,直到找到该异常的处理程序。展开堆栈意味着为由于堆栈帧即将消失而消失的对象调用析构函数。如果其中一个析构函数抛出未在析构函数本身中处理的异常,则会发生此建议的内容。如果发生这种情况,程序会调用std::terminate(),有些人认为发生这种情况的风险非常严重,以至于他们必须编写编码指南来防止这种情况发生。

在您的代码中,这不是问题。B的析构函数引发异常;因此,还会调用a的析构函数。该析构函数引发异常,但在析构函数内处理异常。所以没有问题。

如果更改代码以删除A析构函数中的try ... catch块,则析构函数中引发的异常不会在析构函数中处理,因此最终会调用std::terminate()

编辑:正如Brian在他的回答中指出的那样,这个规则在C++11中发生了变化:析构函数是隐式noexcept的,所以你的代码应该在B对象被销毁时调用terminate。将析构函数标记为noexcept(false)"修复"此问题。