C++析构函数中的异常

13

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

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

输出结果为:

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

8
在你的 B b; 后面尝试插入 throw 42; - Fred Larson
2
实际上你提供的方法对我没有起作用。我收到了 terminate called after throwing an instance of 'int' 的错误提示。 - Fred Larson
6
以下的例子确实可以生效。我希望人们停止试图证明如果他们以某种方式编写代码,就不存在未定义行为。 - PaulMcKenzie
3
@PaulMcKenzie - <iostream> 不是定义 printf 的必需头文件,但可以使用它。 - Pete Becker
2
@PaulMcKenzie - 鉴于这个问题中有很多错误的评论和答案,询问真正的规则似乎是完全合适的。 - Pete Becker
显示剩余8条评论
2个回答

26

在 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()来检测对象是否在堆栈展开期间被销毁。但你最好知道你在做什么。


2
完全证实了在C++03模式下编译给出OP的结果,而在C++14模式下编译调用terminate。在这种情况下,g++ 6.2甚至会给出一个很好的警告:“warning: throw will always call terminate() [-Wterminate] note: in C++11 destructors default to noexcept”。 - Matteo Italia

18
“不应在析构函数中抛出异常”的建议并非绝对。问题在于,当抛出异常时,编译器开始展开堆栈,直到找到该异常的处理程序为止。展开堆栈意味着调用那些将要消失的对象的析构函数,因为它们的堆栈框架即将消失。而这个建议所涉及的问题发生在如果其中一个析构函数抛出一个未在析构函数内部处理的异常时。如果发生了这种情况,程序会调用std::terminate(),一些人认为这种情况的风险如此严重,以至于他们必须编写编码指南以防止它发生。
在您的代码中,这不是问题。对于B的析构函数抛出异常;因此也调用了a的析构函数。该析构函数抛出异常,但在析构函数内部处理了异常。所以没有问题。
如果您更改代码以删除A的析构函数中的try...catch块,则析构函数中抛出的异常未在析构函数中处理,因此最终会调用std::terminate()
编辑:正如Brian在他的答案中指出的那样,C++11中更改了这个规则:析构函数默认情况下是noexcept的,因此当销毁B对象时,您的代码应该调用terminate。将析构函数标记为noexcept(false)可以“修复”这个问题。

虽然这是一些好知识,但通常来说,你不想要有抛出异常的析构函数;虽然有些情况下(比如这个)你可以让它工作,但需要极度小心,并且会严重限制你对该对象的操作(例如,在由于另一个异常而进行退回时不能保证其安全地被销毁,并且不能将其安全地放入STL容器中)。 - Matteo Italia
参见此答案或C++标准15.2(引用自工作草案):“(...) 3.从try块到throw表达式的路径上构造的自动对象的析构调用过程称为“堆栈展开”。如果在堆栈展开期间调用的析构函数以异常退出,则会调用std::terminate(15.5.1)。[注意:因此,析构函数通常应捕获异常,而不是让它们传播出去。-注]” - Sonic78
“没有问题”——除非稍后有其他人尝试以完全正常的方式使用此类。这个类就像陷阱一样简单明了。“C允许你朝自己的脚开枪,而C++则允许你重复使用子弹。” - ech
1
@ech -- 请不要断章取义。"在你的代码中,这不是一个问题。[以下是发生的情况]所以没有问题。" 我回答的其余部分讨论了通常会发生的情况,并完全涵盖了让你如此激动的问题。 - Pete Becker
我是在那个语境下这么说的。谢谢! - ech

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