从析构函数中抛出exception

大多数人说永远不会从析构函数中抛出exception – 这样做会导致未定义的行为。 Stroustrup指出: “vector析构函数明确地调用每个元素的析构函数,这意味着如果析构函数抛出,vector销毁失败……真的没有好办法来防止析构函数抛出的exception,所以库如果元素析构函数抛出不作任何保证“(来自附录E3.2)

这篇文章似乎是另有说法 – 抛出析构函数或多或less是可以的。

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

如果在清理操作中发生错误,您是否忽略它? 如果这是一个错误,可能会被处理堆栈,但不是正确的析构函数,是不是有意义抛出exception析构函数?

显然这些错误是罕见的,但可能的。

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

 #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"; } } 

这基本上归结为:

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

然后析构函数通过调用这些方法(如果用户没有明确这么做的话)来完成对象的处理,但是抛出的exception都会被捕获并且被抛弃(在尝试修复这个问题之后)。

所以在影响你把责任传递给用户。 如果用户能够纠正exception,他们将手动调用相应的function并处理任何错误。 如果对象的用户不担心(因为对象将被销毁),那么parsing器将被留下来处理业务。

一个例子:

的std :: fstream的

close()方法可能会抛出exception。 如果文件已被打开,析构函数调用close(),但确保任何exception不会传播出析构函数。

因此,如果文件对象的用户想要对与closures文件相关的问题进行特殊处理,他们将手动调用close()并处理任何exception。 另一方面,如果他们不在乎那么解构者将被留下来处理这种情况。

Scott Myers在他的书“Effective C ++”中有一篇关于这个主题的优秀文章,

编辑:

显然,在“更有效的C ++”
项目11:防止exception离开析构函数

抛出析构函数可能导致崩溃,因为这个析构函数可能被称为“堆栈展开”的一部分。 堆栈展开是在抛出exception时发生的过程。 在这个过程中,所有从“try”到被抛出的exception被推入堆栈的对象都将被终止 – >它们的析构函数将被调用。 在这个过程中,另一个exception抛出是不允许的,因为一次不能处理两个exception,所以这将引发一个调用abort(),程序将崩溃,控制权将返回到操作系统。

我们必须在此区分 ,而不是盲目地遵循针对具体情况的一般性build议。

请注意,以下内容忽略了对象容器的问题,以及面对容器内多个对象时要做什么。 (部分对象可能会被忽略,因为有些对象不适合放入容器。)

当我们以两种types拆分类时,整个问题变得更容易思考。 一个类可以有两个不同的职责:

  • (R)释放语义(aka free memory)
  • (C) 提交语义(aka flush file to disk)

如果我们这样看待这个问题,那么我认为可以认为(R)语义学不应该引起一个例外,因为我们没有办法做到这一点,而且b)许多自由资源操作不甚至提供错误检查,例如void free(void* p);

具有(C)语义的对象,如需要成功刷新其数据的文件对象或在Dtor中进行提交的(“范围防护”)数据库连接,具有不同的types:我们可以对错误进行操作应用程序级别),我们真的不应该继续下去,就好像什么都没有发生

如果我们遵循RAII的路线,并且允许在其中有(C)个语义的对象,那么我们也就必须考虑到这种可能抛出的奇怪情况。 因此,你不应该把这样的对象放到容器中,而且如果在另一个exception处于活动状态时抛出commit-dtor,程序仍然可以terminate()


关于error handling(提交/回滚语义)和exception, Andrei Alexandrescu介绍了一个很好的演讲: C ++ /声明控制stream程中的error handling (在NDC 2014举行)

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

(我应该注意到其他人也有类似的想法。)

虽然谈话并不关注掷骰子,但它显示了一个可以用来摆脱抛出时间的问题的工具。

未来可能会有一个标准的function, 参见N3614 ,并讨论它 。

Upd '17:C ++ 17标准function是std::uncaught_exceptions afaikt。 我会尽快引用cppref文章:

笔记

一个使用int -returning uncaught_exceptions的例子是……首先创build一个guard对象,并在其构造函数中logging未捕获exception的数量。 输出由guard对象的析构函数执行,除非foo()抛出( 在这种情况下,析构函数中未捕获的exception的数量大于构造函数的观察值

问问你自己从一个析构函数抛出的真正问题是“调用者可以用这个来做什么?” 有没有什么有用的,你可以做例外,这将抵消从析构函数投掷所造成的危险?

如果我摧毁一个Foo对象,并且Foo析构函数抛出一个exception,我可以合理地使用它吗? 我可以login,或者我可以忽略它。 就这样。 我不能“修复”它,因为Foo对象已经消失了。 最好的情况下,我loggingexception,并继续,如果没有发生任何事情(或终止程序)。 从析构函数中抛出是否真的有可能导致未定义的行为?

它是危险的,但从可读性/代码可理解性的angular度来看也是没有意义的。

你要问的是在这种情况下

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

什么应该抓住例外? 应该是foo的调用者吗? 还是应该处理呢? 为什么foo的调用者关心foo内部的某个对象? 这种语言可能有一种定义是有道理的,但是它会变得难以理解,难以理解。

更重要的是,Object的内存在哪里去? 对象拥有的内存在哪里去? 它仍然分配(表面上是因为析构失败)? 考虑到对象是在堆栈空间 ,所以它显然不pipe。

然后考虑这种情况

 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

从ISO草案C ++(ISO / IEC JTC 1 / SC 22 N 4411)

所以析构函数通常应该捕获exception,而不是让它们从析构函数中传播出去。

3从try块到throw-expression式的path上构造的自动对象的析构函数调用的过程称为“堆栈展开”。[注意:如果在堆栈展开期间调用的析构函数退出并且有一个exception,则调用std :: terminate (15.5.1)。 所以析构函数通常应该捕获exception,而不是让它们从析构函数中传播出去。 – 结束注意]

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

其他人已经解释了为什么抛出破坏者是可怕的…你可以做些什么呢? 如果您正在执行可能失败的操作,请创build一个单独的公用方法来执行清理,并可以抛出任意exception。 在大多数情况下,用户将忽略这一点。 如果用户想要监视清理的成功/失败,他们可以简单地调用显式清理例程。

例如:

 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 (eg file is open by another process) // the rest of the class omitted... }; 

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

文章采取了“抛出exception有什么替代scheme”这一行,并列出了每个备选scheme的一些问题。 这样做的结论是,因为我们无法find一个无问题的select,我们应该继续抛出exception。

麻烦的是,它所列出的问题没有任何一个与exception行为一样糟糕,我们记得,是“程序的未定义行为”。 一些作者的反对意见包括“审美丑陋”和“鼓励不良风格”。 现在你想要哪一个? 一个风格不好的程序,或一个performance出不确定行为的程序?

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

答:有几个选项:

  1. 让exceptionstream出你的析构函数,而不pipe其他地方发生了什么。 在这样做的时候要注意(或者甚至害怕)std :: terminate可能跟在后面。

  2. 永远不要让exceptionstream出你的析构函数。 如果可以的话,可能会写入日志,有些大的红色坏文本。

  3. 我的爱人 :如果std::uncaught_exception返回false,让你exceptionstream出。 如果它返回true,则返回到日志logging方法。

但是投掷是好的吗?

我同意上面的大多数,抛出是最好的避免在析构函数,它可以是。 但有时你最好接受它可能发生,并处理好。 我会select3以上。

有一些奇怪的情况是从析构函数中抛出一个好主意 。 像“必须检查”错误代码一样。 这是从函数返回的值types。 如果调用者读取/检查包含的错误代码,则返回的值将自动破坏。 但是 ,如果在返回值超出范围时返回的错误代码还没有被读取,它将从析构函数中抛出一些exception。

我现在遵循的政策(很多人说),class级不应该积极地从他们的析构者抛出exception,而应该提供一个公开的“closures”方法来执行可能会失败的操作。

…但我确实相信容器类类的析构函数,就像一个向量,不应该屏蔽从它们包含的类抛出的exception。 在这种情况下,我实际上使用了一个“自由/closures”的方法recursion调用自己。 是的,我recursion地说。 这种疯狂有一种方法。 exception传播依赖于存在堆栈:如果发生单个exception,那么剩余的析构函数仍将运行,并且一旦例程返回,挂起的exception就会传播,这非常好。 如果发生多个exception,那么(取决于编译器)第一个exception将传播或程序将终止,这是可以的。 如果发生这么多的例外,recursion溢出堆栈,那么有些事情是严重错误的,有人会发现它,这也是可以的。 就我个人而言,我犯的错误是错误的,而不是隐藏的,秘密的和阴险的。

关键是容器保持中立,由被包含的类决定他们是否在从析构函数中抛出exception的行为或行为不当。

我在这个小组里认为,在许多情况下,抛出析构函数的“范围守卫”模式是有用的,特别是在unit testing中。 但是请注意,在C ++ 11中,抛出一个析构函数会导致对std::terminate的调用,因为析构函数被隐式地注释为noexcept

AndrzejKrzemieński在有关析构函数的话题上写了一篇很棒的文章:

他指出,C ++ 11有一个机制来覆盖析构函数的默认noexcept

在C ++ 11中,析构函数被隐式指定为noexcept 。 即使你不添加任何规范,并像这样定义你的析构函数:

  class MyType { public: ~MyType() { throw Exception(); } // ... }; 

编译器将仍然无形地添加规范noexcept到你的析构函数。 这意味着,当析构函数抛出一个exception时,即使没有双重exception情况, std::terminate也会被调用。 如果你真的决定允许你的析构函数抛出,你必须明确地指定它; 你有三个select:

  • 显式指定你的析构函数为noexcept(false)
  • 从已经指定了析构函数的另一个类inheritancenoexcept(false)
  • 把已经指定了析构函数的非静态数据成员放在你的类中,作为noexcept(false)

最后,如果你决定抛出析构函数,你应该总是意识到双重exception的风险(当堆栈正在放松,因为exception而抛出)。 这会导致对std::terminate的调用,而且很less有你想要的。 为了避免这种行为,你可以简单地检查在使用std::uncaught_exception()抛出一个新的之前是否已经有一个exception。

设置闹钟事件。 通常,警报事件是清理对象时通知故障的更好forms

与构造函数不同的是,抛出exception可能是指示对象创build成功的有用方法,不应该在析构函数中抛出exception。

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

因此,最好的行动就是不要在破坏者中完全使用exception。 写一条消息到日志文件。

Martin Ba(上图)正处于正确的轨道上 – 对于RELEASE和COMMIT逻辑,您的架构是不同的。

发布:

你应该吃任何错误。 您正在释放内存,closures连接等等。系统中的其他人都不会再看到这些东西,而是将资源交给操作系统。 如果看起来你需要真正的error handling,这可能是你的对象模型中的devise缺陷的后果。

对于Commit:

这就是你想要的类似RAII包装对象的地方,像std :: lock_guard一样提供互斥体。 与那些你不把提交逻辑放在Dtor AT ALL中。 你有一个专用的API,然后包装对象,RAII将它提交给它们并处理那里的错误。 记住,你可以在析构函数中捕获exception就好了; 它的发行是致命的。 这也可以让你实现策略和不同的error handling,只是build立一个不同的包装(例如std :: unique_lock与std :: lock_guard),并确保你不会忘记调用提交逻辑 – 这是唯一的中途把它放在一个地方的正当理由。