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

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

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

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

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


当前回答

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()在抛出一个新的异常之前检查是否已经有一个异常。

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

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

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

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

请注意,下面的内容忽略了对象容器的问题,以及在容器内存在多个对象的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()抛出(在这种情况下,未捕获的数量 析构函数中的异常大于构造函数中的异常 观察到)

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

如果我销毁了一个Foo对象,而Foo析构函数抛出了一个异常,我可以合理地对它做什么?我可以记录,也可以忽略。这是所有。我不能“修复”它,因为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.