析构函数中的 C++ 异常

c++ exception in destructor

提问人:beetlej 提问时间:12/16/2016 更新时间:12/16/2016 访问量:9836

问:

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

#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
C++ 异常

评论

8赞 Fred Larson 12/16/2016
尝试在您的 .throw 42;B b;
2赞 Fred Larson 12/16/2016
实际上,你所拥有的对我不起作用。我得到了.terminate called after throwing an instance of 'int'
6赞 PaulMcKenzie 12/16/2016
但在下面的例子中,它确实有效——我希望人们不要再试图证明,如果他们以某种方式编写代码,就不存在未定义的行为。
3赞 Pete Becker 12/16/2016
@PaulMcKenzie - 不需要定义,但允许定义。<iostream>printf
2赞 Pete Becker 12/16/2016
@PaulMcKenzie - 鉴于对这个问题的评论和答案的数量是完全错误的,问问真正的规则是什么似乎是完全合适的。

答:

18赞 Pete Becker 12/16/2016 #1

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

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

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

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

评论

0赞 Matteo Italia 12/16/2016
很高兴知道 - 但总的来说,你确实不希望析构函数抛出;尽管在某些情况下(像这样)您可以使其工作,但它需要非常小心,并且严重限制了您可以对给定对象执行的操作(例如,由于另一个异常,您不能指望它在展开过程中被安全销毁,并且您不能将其安全地放入 STL 容器中)。
0赞 Sonic78 10/6/2017
另请参阅此答案或 15.2 的 C++ 标准(引用自工作草案):“(...3. 为在从 try 块到 throw-expression 的路径上构造的自动对象调用析构函数的过程称为“堆栈展开”。如果在堆栈展开期间调用的析构函数退出并出现异常,则调用 std::terminate (15.5.1)。[注意:因此,析构函数通常应该捕获异常,而不是让它们从析构函数中传播出去。'
0赞 ech 2/27/2019
“没问题”——除非其他人以后尝试以完全正常的方式使用这个类。这个类是一个陷阱,简单明了。“C允许你搬起石头砸自己的脚。C++ 允许你重用项目符号。
1赞 Pete Becker 2/27/2019
@ech -- 请不要断章取义。“在你的代码中,这不是问题。[这是发生的事情]所以没有问题。我回答的其余部分谈到了一般发生的事情,并完全涵盖了让你如此努力的问题。
0赞 ech 2/27/2019
我是在那种情况下说的。谢谢!
26赞 Brian Bi 12/16/2016 #2

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

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

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

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

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

在 C++17 中,可用于检测在堆栈展开期间是否正在销毁对象。但你最好知道你在做什么。std::uncaught_exceptions()

评论

2赞 Matteo Italia 12/16/2016
在 C++03 模式下编译会给出 OP 的结果,而在 C++14 模式下编译调用会终止,这一事实完全证实了这一点。在这种情况下,g++ 6.2 甚至给出了一个很好的警告“警告:throw 将始终调用 terminate() [-Wterminate] 注意:在 C++11 析构函数中默认为 noexcept”。