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

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

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

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

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


从析构函数抛出异常是危险的。 如果另一个异常已经在传播,则应用程序将终止。

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

这基本上可以归结为:

任何危险的事情(例如,可能抛出异常)都应该通过公共方法来完成(不一定直接)。然后,类的用户可以通过使用公共方法并捕获任何潜在的异常来潜在地处理这些情况。

析构函数将通过调用这些方法(如果用户没有显式地这样做)来结束对象,但是任何抛出的异常都会被捕获并丢弃(在尝试修复问题之后)。

所以实际上你把责任转嫁给了用户。如果用户处于纠正异常的位置,他们将手动调用适当的函数并处理任何错误。如果对象的用户不担心(因为对象将被销毁),则剩下析构函数来处理事务。

一个例子:

std:: fstream

close()方法可能会抛出异常。 如果文件已打开,析构函数将调用close(),但要确保任何异常都不会从析构函数传播出去。

因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,他们将手动调用close()并处理任何异常。另一方面,如果它们不关心,那么析构函数将被留下来处理这种情况。

Scott Myers在他的书《Effective c++》中有一篇关于这个主题的优秀文章。

编辑:

显然在“更有效的c++”中也有 项目11:防止异常离开析构函数

你的析构函数可能在其他析构函数链中执行。抛出未被立即调用者捕获的异常可能会使多个对象处于不一致的状态,从而导致更多的问题,而不是在清理操作中忽略错误。

关于从析构函数抛出,真正要问自己的问题是“调用者可以用它做什么?”你是否真的可以对异常做一些有用的事情,来抵消从析构函数抛出的危险?

如果我销毁了一个Foo对象,而Foo析构函数抛出了一个异常,我可以合理地对它做什么?我可以记录,也可以忽略。这是所有。我不能“修复”它,因为Foo对象已经消失了。最好的情况是,我记录异常并继续,就像什么都没有发生一样(或者终止程序)。这真的值得通过从析构函数抛出来潜在地引起未定义的行为吗?

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

你要问的是在这种情况下

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

Throwing out of a destructor can result in a crash, because this destructor might be called as part of "Stack unwinding". Stack unwinding is a procedure which takes place when an exception is thrown. In this procedure, all the objects that were pushed into the stack since the "try" and until the exception was thrown, will be terminated -> their destructors will be called. And during this procedure, another exception throw is not allowed, because it's not possible to handle two exceptions at a time, thus, this will provoke a call to abort(), the program will crash and the control will return to the OS.

其他人都解释了为什么抛出析构函数很糟糕……你能做些什么呢?如果您正在执行一个可能失败的操作,请创建一个单独的公共方法来执行清理,并可以抛出任意异常。在大多数情况下,用户会忽略这一点。如果用户希望监视清理的成功/失败,他们可以简单地调用显式清理例程。

例如:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

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

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

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

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

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

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

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

...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.

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

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

A:有几种选择:

让异常流出析构函数,而不管其他地方发生了什么。在这样做的时候,要意识到(甚至害怕)std::terminate可能会随之而来。 永远不要让异常从析构函数流出。可能是写一个日志,一些大红色坏文本,如果可以的话。 我的最爱:如果std::uncaught_exception返回false,让你的异常流出。如果返回true,则退回到日志记录方法。

但加进d'tors好吗?

我同意上面的大部分观点,在析构函数中最好避免抛出,因为它可以在析构函数中抛出。但有时你最好接受它的发生,并妥善处理。我选择上面的3。

在一些奇怪的情况下,从析构函数抛出实际上是个好主意。 比如“必须检查”错误代码。这是一个从函数返回的值类型。如果调用者读取/检查包含的错误代码,返回值将静默销毁。 但是,如果返回值超出作用域时还没有读取返回的错误代码,则它将从析构函数抛出一些异常。

在这里,我们必须有所区分,而不是盲目地按照具体情况的一般建议行事。

请注意,下面的内容忽略了对象容器的问题,以及在容器内存在多个对象的d' ator时该怎么办。(它可以被部分忽略,因为有些对象就是不适合放入容器中。)

当我们把类分成两种类型时,整个问题就更容易思考了。类医生可以有两个不同的职责:

(R)释放语义(也就是释放内存) (C)提交语义(即将文件刷新到磁盘)

如果我们以这种方式看待这个问题,那么我认为(R)语义永远不应该引起来自dtor的异常,因为a)我们对此无能为力,b)许多自由资源操作甚至不提供错误检查,例如void free(void* p);。

具有(C)语义的对象,例如需要成功刷新其数据的文件对象或在dtor中执行提交的(“范围保护”)数据库连接是另一种类型:我们可以对错误(在应用程序级别)做一些事情,并且我们真的不应该继续,就像什么都没有发生一样。

如果我们遵循RAII路线,并允许在d'tors中具有(C)语义的对象,我认为我们还必须允许这种d'tors可以抛出的奇数情况。因此,您不应该将此类对象放入容器中,并且如果committor在另一个异常活动时抛出,则程序仍然可以terminate()。


关于错误处理(提交/回滚语义)和异常,Andrei Alexandrescu有一个很好的演讲:c++ /声明性控制流中的错误处理(在NDC 2014举行)

在细节中,他解释了Folly库如何为他们的ScopeGuard工具实现UncaughtExceptionCounter。

(我应该指出,其他人也有类似的想法。)

虽然这次演讲的重点不是从一个d'tor投掷,但它展示了一个今天可以用来解决何时从d'tor投掷问题的工具。

在未来,可能会有一个std特性,请参阅N3614,并对此进行讨论。

Upd '17: c++ 17的std特性是std::uncaught_exceptions afaikt。我将快速引用cppref的文章:

笔记 使用int返回uncaught_exceptions的示例是... ...第一个 创建一个保护对象并记录未捕获异常的数量 在它的构造函数中。输出是由守卫对象执行的 析构函数,除非foo()抛出(在这种情况下,未捕获的数量 析构函数中的异常大于构造函数中的异常 观察到)

设置告警事件。通常,警报事件是在清理对象时通知失败的更好形式

与构造函数不同,在构造函数中抛出异常是指示对象创建成功的有用方法,而在析构函数中不应抛出异常。

当在堆栈展开过程中从析构函数抛出异常时,就会发生问题。如果发生这种情况,编译器将处于不知道是继续堆栈展开过程还是处理新异常的情况。最终的结果是,您的程序将立即终止。

因此,最好的做法就是完全避免在析构函数中使用异常。相反,将消息写入日志文件。

Martin Ba(上图)在正确的轨道上——你为RELEASE和COMMIT逻辑构建了不同的架构。

发布:

你应该吃任何错误。您正在释放内存、关闭连接等。系统中的任何人都不应该再看到这些东西,并且您正在将资源交还给操作系统。如果你看起来需要真正的错误处理,这可能是你的对象模型设计缺陷的结果。

提交:

This is where you want the same kind of RAII wrapper objects that things like std::lock_guard are providing for mutexes. With those you don't put the commit logic in the dtor AT ALL. You have a dedicated API for it, then wrapper objects that will RAII commit it in THEIR dtors and handle the errors there. Remember, you can CATCH exceptions in a destructor just fine; its issuing them that's deadly. This also lets you implement policy and different error handling just by building a different wrapper (e.g. std::unique_lock vs. std::lock_guard), and ensures you won't forget to call the commit logic- which is the only half-way decent justification for putting it in a dtor in the 1st place.

我所在的小组认为,在析构函数中加入“作用域保护”模式在许多情况下都很有用——特别是对于单元测试。但是,要注意,在c++ 11中,抛出析构函数会导致调用std::terminate,因为析构函数隐式地用noexcept注释。

Andrzej krzemiezynski有一篇关于抛出析构函数的文章:

https://akrzemi1.wordpress.com/2011/09/21/destructors-that-throw/

他指出c++ 11有一种机制可以覆盖析构函数的默认noexcept:

In C++11, a destructor is implicitly specified as noexcept. Even if you add no specification and define your destructor like this: class MyType { public: ~MyType() { throw Exception(); } // ... }; The compiler will still invisibly add specification noexcept to your destructor. And this means that the moment your destructor throws an exception, std::terminate will be called, even if there was no double-exception situation. If you are really determined to allow your destructors to throw, you will have to specify this explicitly; you have three options: Explicitly specify your destructor as noexcept(false), Inherit your class from another one that already specifies its destructor as noexcept(false). Put a non-static data member in your class that already specifies its destructor as noexcept(false).

最后,如果您决定抛出析构函数,则应该始终注意双异常的风险(在堆栈因异常而被unwind时抛出)。这将导致调用std::terminate,这很少是您想要的。为了避免这种行为,你可以使用std::uncaught_exception()在抛出一个新的异常之前检查是否已经有一个异常。

我的问题是,如果从析构函数抛出会导致 未定义的行为,如何处理过程中发生的错误 析构函数?

主要的问题是:你不能失败到失败。失败到底意味着什么?如果将事务提交到数据库失败,并且事务未能失败(回滚失败),那么数据的完整性会发生什么变化?

由于析构函数在正常路径和异常(失败)路径上都被调用,它们本身不能失败,否则我们就是“失败”。

这是一个概念上很困难的问题,但通常解决方案是找到一种方法来确保失败不会失败。例如,数据库可能会在提交到外部数据结构或文件之前写入更改。如果事务失败,则可以丢弃文件/数据结构。它必须确保从外部结构/文件提交的更改是一个不会失败的原子事务。

务实的解决办法也许就是确保 一次又一次的失败在天文学上是不可能的,因为做东西 在某些情况下,失败几乎是不可能的。

对我来说,最合适的解决方案是以一种清理逻辑不会失败的方式编写非清理逻辑。例如,如果您想要创建一个新的数据结构来清理现有的数据结构,那么您可能会寻求提前创建那个辅助结构,这样我们就不必在析构函数中创建它了。

This is all much easier said than done, admittedly, but it's the only really proper way I see to go about it. Sometimes I think there should be an ability to write separate destructor logic for normal execution paths away from exceptional ones, since sometimes destructors feel a little bit like they have double the responsibilities by trying to handle both (an example is scope guards which require explicit dismissal; they wouldn't require this if they could differentiate exceptional destruction paths from non-exceptional ones).

Still the ultimate problem is that we can't fail to fail, and it's a hard conceptual design problem to solve perfectly in all cases. It does get easier if you don't get too wrapped up in complex control structures with tons of teeny objects interacting with each other, and instead model your designs in a slightly bulkier fashion (example: particle system with a destructor to destroy the entire particle system, not a separate non-trivial destructor per particle). When you model your designs at this kind of coarser level, you have less non-trivial destructors to deal with, and can also often afford whatever memory/processing overhead is required to make sure your destructors cannot fail.

And that's one of the easiest solutions naturally is to use destructors less often. In the particle example above, perhaps upon destroying/removing a particle, some things should be done that could fail for whatever reason. In that case, instead of invoking such logic through the particle's dtor which could be executed in an exceptional path, you could instead have it all done by the particle system when it removes a particle. Removing a particle might always be done during a non-exceptional path. If the system is destroyed, maybe it can just purge all particles and not bother with that individual particle removal logic which can fail, while the logic that can fail is only executed during the particle system's normal execution when it's removing one or more particles.

如果避免使用非平凡析构函数处理大量小对象,通常会出现这样的解决方案。当你被大量的小对象纠缠在一起时,你可能会陷入混乱,似乎不可能是异常安全的,这些小对象都有非平凡的dtor。

如果任何指定nothrow/noexcept的函数(包括应该继承其基类的noexcept规范的虚函数)试图调用任何可能抛出的函数,那么nothrow/noexcept实际上会被翻译成编译器错误,这将会有很大帮助。这样我们就能在编译时捕获所有这些东西,如果我们实际上无意中编写了一个析构函数,它可能会抛出。

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

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.