【问题标题】:Why I couldn't handle an exception thrown from a destructor outside of it?为什么我无法处理从外部析构函数抛出的异常?
【发布时间】:2021-05-27 03:52:41
【问题描述】:

我在 C++ 入门第 5 版第 18 章的异常处理中读到,析构函数通常不应该像 STL 容器那样抛出异常,如果应该抛出的表达式应该包装在 try-catch 块中(catch 必须处理那个例外)。

  • 为了理解目的,我已经尝试过这个:

      struct Foo{
          Foo(){
              std::cout << "Foo()\n";
          }
          ~Foo(){
              std::cout << "~Foo()\n";
              throw "exception thrown from Foo::~Foo()\n";
              std::cout << "Back in Foo::~Foo()\n";
          }
      };
    
    
      int main(){
    
          try{
              Foo f{};
              Foo* pFoo = new Foo{};
              delete pFoo; // normally exception caught here nad handled by the following catch
          }
          catch(char const* const& cp){
              std::cout << cp << '\n';
          }
    
          std::cout << '\n';
      }
    
  • 恕我直言Foo 的析构函数不应该在这里抛出该异常,因为析构函数之外的处理程序永远无法捕获该异常;因为每当对象超出范围并且try-catch块总是在块内时都会调用析构函数,换句话说,析构函数是在try-catch blcok之后调用的。

  • 但正如我们所知,我们可以过早地调用析构函数,例如显式调用它或删除指向有效动态内存的类/结构类型的指针。所以在这种情况下,我们可以在 try-block 中调用该析构函数,通常可以由相应的处理程序处理。

  • 但是我的程序总是调用terminate()(作为未处理异常的标志)并且我收到警告:

warning: throw will always call 'terminate' [-wterminate].

  • 如您所见,我在try-block 中调用Foo 析构函数main:delete Foo。如果我写的话,我也可以这样做:f.~Foo();

  • 那么有人能解释一下这背后的原因吗?谢谢!

【问题讨论】:

  • 我之前和现在删除的评论是一个错误。您将 try/catch 放置在错误的位置。析构函数一般不应该抛出。如果必须,则必须在析构函数本身处理。例外是针对特殊情况。特殊情况往往会排除析构函数,因为析构函数完成它们的工作。
  • 由于 C++11 的析构函数中的异常是被禁止的,除非析构函数被显式声明为 noexcept(false)。析构函数被隐式声明为noexcept(true)
  • 仅供参考:C++ FAQ: How can I handle a destructor that fails?将消息写入日志文件。终止进程。或者打电话给蒂尔达阿姨。但不要抛出异常!
  • 至于析构函数中的异常不好的原因,假设其他一些代码抛出了异常。在从该初始异常中展开调用堆栈时,本地对象的破坏会引发第二个嵌套异常,您无法捕获
  • 旁注:g++ 6 或更高版本给出了一个很好的提示:注意:在 C++11 中,析构函数默认为 'noexcept'。用 Godbolt 戳了一下,看起来 clang 5+ 给出了类似的注释。

标签: c++ exception


【解决方案1】:

我认为您误解了建议:

析构函数通常不应该像 STL 容器那样抛出异常,如果应该抛出的表达式应该包装在 try-catch 块中(catch 必须处理该异常)。

换句话说,它说:

你不应该从析构函数中抛出异常。

现在问题来了,如果析构函数是:

~Bar() {  
   do_something();
}

do_something() 可能会抛出异常。在这种情况下,建议是捕获异常并处理它:

~Bar() {
    try {
        do_something();
    } catch(...) {
       // handle exception, eg write a log message
       // but do not retrow it or a different one!
    }
}

现在异常无法离开析构函数。


正如您所发现的,在析构函数之外捕获异常无助于解决一般问题,因为这里:

 try{
          Foo f{};
          Foo* pFoo = new Foo{};
          delete pFoo; // normally exception caught here nad handled by the following catch
      }

delete pFoo; 的调用会引发异常。在堆栈展开期间,Foo f{}; 将被销毁,并且从析构函数中抛出另一个异常。如果在堆栈展开期间抛出异常,std::terminate 将被调用。避免这种情况发生的方法是不要从析构函数中抛出异常。

【讨论】:

  • 构造函数也可以引发异常,但是为什么可以从 ctor 而不是 dtor 呢?
  • @Maestro 因为抛出异常确实会触发析构函数被调用,如果它们再次抛出,你就会遇到问题。抛出异常很少会触发对构造函数的调用。
  • @Maestro 在以下情况下抛出异常:a)您无法在问题发生的地方处理问题,并且 b)问题可以在调用堆栈的某个位置处理。使用析构函数 b) 不成立,因为在最坏的情况下,没有人可以捕捉到异常并且调用了 std::terminate。这就是为什么您应该重新考虑 a) 并直接在析构函数中处理异常。对于构造函数,情况并非如此。
  • 好的,现在我已经正确理解了!因为正如你所说:当 pFoo 被删除时,它的 dtor 被调用,它在不处理它的情况下抛出,所以控制让 Foo 的 dtor 在调用链中搜索一个处理程序。所以 try 块被留下,因此所有本地对象都被销毁,所以这里 f dtor 被调用。但它再次抛出,所以我们在堆栈展开期间有 2 个异常调用“终止”。如果只有一个对象并且 dtor 是 noexcept(false) ,那么它看起来还可以,但很危险。此问题不会触发 ctors,因为它们不会像 dtors 那样自动调用。非常感谢!
猜你喜欢
  • 2020-02-24
  • 2014-08-12
  • 1970-01-01
  • 2010-12-25
  • 2015-08-26
  • 1970-01-01
  • 1970-01-01
  • 2022-01-16
相关资源
最近更新 更多