大多数人说永远不要从析构函数抛出异常——这样做会导致未定义的行为。Stroustrup指出,“vector析构函数显式地为每个元素调用析构函数。这意味着如果元素析构函数抛出,则vector销毁失败…实际上没有很好的方法来防止析构函数抛出异常,因此标准库不保证元素析构函数是否抛出”(摘自附录E3.2)。

这篇文章似乎不是这么说的——抛出析构函数或多或少是可以的。

所以我的问题是,如果从析构函数抛出导致未定义的行为,你如何处理析构函数期间发生的错误?

如果在清理操作期间发生错误,您会忽略它吗?如果它是一个可以在堆栈中处理但不能在析构函数中处理的错误,那么从析构函数抛出异常难道没有意义吗?

显然,这种错误很少见,但也有可能发生。


当前回答

作为对主要答案的补充,这些答案是好的,全面的和准确的,我想评论一下你引用的文章——那篇文章说“在析构函数中抛出异常并不是那么糟糕”。

本文以“抛出异常的替代方法是什么”为题,并列举了每种替代方法的一些问题。这样做后,它得出结论,因为我们找不到一个没有问题的替代方案,所以我们应该继续抛出异常。

问题在于,它列出的所有问题都没有异常行为那么糟糕,让我们记住,异常行为是“程序的未定义行为”。作者的一些反对意见包括“美学上的丑陋”和“鼓励糟糕的风格”。现在你想要哪一个?一个风格糟糕的程序,还是一个表现出未定义行为的程序?

其他回答

这很危险,但从可读性/代码可理解性的角度来看,这也没有意义。

你要问的是在这种情况下

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

什么应该捕获异常?foo的调用者应该这样做吗?或者应该由foo来处理它?为什么foo的调用者要关心foo内部的某个对象?可能有一种语言定义的方式是有意义的,但它将是不可读的和难以理解的。

更重要的是,对象的内存去哪里了?对象拥有的内存到哪里去了?它仍然被分配吗(表面上是因为析构函数失败了)?考虑到对象在堆栈空间中,所以它显然已经消失了。

然后考虑这个例子

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

当obj3的删除失败时,我如何以一种保证不会失败的方式删除?该死的是我的记忆!

现在考虑在第一个代码片段中,Object自动消失,因为它在堆栈上,而Object3在堆上。因为指向Object3的指针没有了,你就有点SOL了,你有内存泄漏。

下面是一种安全的做法

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

也可以参考这个FAQ

c++的ISO草案(ISO/IEC JTC 1/SC 22 N 4411)

因此,析构函数通常应该捕获异常,而不是让它们从析构函数传播出去。

为在try块到throw-的路径上构造的自动对象调用析构函数的过程 表达式称为“堆栈unwind”。[注意:如果在堆栈展开期间调用析构函数退出 异常,std::terminate被调用(15.5.1)。因此,析构函数通常应该捕获异常,而不是let 它们从析构函数中传播出去。-结束注]

从析构函数抛出异常永远不会导致未定义的行为。

The problem of throwing exceptions out a destructor is that destructors of successfully created objects which scopes are leaving while handling an uncaught exception (it is after an exception object is created and until completion of a handler of the exception activation), are called by exception handling mechanism; and, If such additional exception from the destructor called while processing the uncaught exception interrupts handling the uncaught exception, it will cause calling std::terminate (the other case when std::exception is called is that an exception is not handled by any handler but this is as for any other function, regardless of whether or not it was a destructor).


如果正在处理未捕获的异常,您的代码永远不知道附加的异常是否会被捕获,或者是否会归档未捕获的异常处理机制,因此永远不知道抛出异常是否安全。

但是,有可能知道正在处理未捕获的异常(https://en.cppreference.com/w/cpp/error/uncaught_exception),因此可以通过检查条件来过度处理,只在情况不是这样的情况下抛出(在某些情况下它不会抛出,但它是安全的)。

但在实践中,将程序分成两种可能的行为并没有什么用——它只是无助于你设计出一个设计良好的程序。


If you throw out of destructors ignoring whether or not an uncaught exception handling is in progress, in order to avoid possible calling std::terminate, you must guarantee that all exceptions thrown during lifetime of an object that may throw an exception from their destructor are caught before beginning of destruction of the object. It is quite limited usage; you hardly can use all classes which would be reasonably allowed to throw out of their destructor in this way; and a combination of allowing such exceptions only for some classes with such restricted usage of these classes impede making a well-designed program, too.

作为对主要答案的补充,这些答案是好的,全面的和准确的,我想评论一下你引用的文章——那篇文章说“在析构函数中抛出异常并不是那么糟糕”。

本文以“抛出异常的替代方法是什么”为题,并列举了每种替代方法的一些问题。这样做后,它得出结论,因为我们找不到一个没有问题的替代方案,所以我们应该继续抛出异常。

问题在于,它列出的所有问题都没有异常行为那么糟糕,让我们记住,异常行为是“程序的未定义行为”。作者的一些反对意见包括“美学上的丑陋”和“鼓励糟糕的风格”。现在你想要哪一个?一个风格糟糕的程序,还是一个表现出未定义行为的程序?

我目前遵循的策略(很多人都这么说)是,类不应该主动从析构函数抛出异常,而是应该提供一个公共的“关闭”方法来执行可能失败的操作……

...but I do believe destructors for container-type classes, like a vector, should not mask exceptions thrown from classes they contain. In this case, I actually use a "free/close" method that calls itself recursively. Yes, I said recursively. There's a method to this madness. Exception propagation relies on there being a stack: If a single exception occurs, then both the remaining destructors will still run and the pending exception will propagate once the routine returns, which is great. If multiple exceptions occur, then (depending on the compiler) either that first exception will propagate or the program will terminate, which is okay. If so many exceptions occur that the recursion overflows the stack then something is seriously wrong, and someone's going to find out about it, which is also okay. Personally, I err on the side of errors blowing up rather than being hidden, secret, and insidious.

关键是容器保持中立,由所包含的类决定它们是否从析构函数抛出异常。