大多数人都说永远不会从析构函数中抛出异常 - 这样做会导致未定义的行为.Stroustrup指出"向量析构函数显式地为每个元素调用析构函数.这意味着如果元素析构函数抛出,向量破坏失败......实际上没有好的方法来防止从析构函数抛出的异常,所以库如果元素析构函数抛出",则不保证"(来自附录E3.2).
这篇文章似乎另有说法 - 抛出析构函数或多或少都没问题.
所以我的问题是 - 如果从析构函数抛出会导致未定义的行为,那么如何处理析构函数期间发生的错误?
如果在清理操作期间发生错误,您是否只是忽略它?如果它是一个可能在堆栈中处理但在析构函数中不正确的错误,那么从析构函数中抛出异常是否有意义?
显然,这类错误很少见,但可能.
从析构函数中抛出异常是危险的.
如果另一个异常已经传播,则应用程序将终止.
#includeclass 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"; } }
这基本归结为:
任何危险的事情(即可能引发异常)都应该通过公共方法(不一定是直接的)来完成.然后,您的类的用户可以通过使用公共方法并捕获任何潜在的异常来处理这些情况.
然后析构函数将通过调用这些方法来完成对象(如果用户没有明确地这样做),但是捕获和删除任何异常throw(在尝试修复问题之后).
因此,实际上您将责任传递给用户.如果用户能够纠正异常,他们将手动调用相应的函数并处理任何错误.如果对象的用户不担心(因为对象将被销毁),那么析构函数将留下来处理业务.
一个例子:的std :: fstream的
close()方法可能会抛出异常.如果文件已被打开,析构函数会调用close(),但要确保任何异常都不会从析构函数中传播出来.
因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,则他们将手动调用close()并处理任何异常.另一方面,如果他们不关心那么析构函数将留给处理这种情况.
Scott Myers在他的"Effective C++"一书中有一篇关于这个主题的优秀文章.
显然也在"更有效的C++"
项目11:防止异常离开析构函数
抛出析构函数可能会导致崩溃,因为这个析构函数可能会被称为"堆栈展开"的一部分.堆栈展开是在抛出异常时发生的过程.在此过程中,自"try"以及抛出异常之后被推入堆栈的所有对象将被终止 - >将调用它们的析构函数.并且在此过程中,不允许另一个异常抛出,因为一次不能处理两个异常,因此,这将引发对abort()的调用,程序将崩溃并且控制将返回到OS.
我们必须在这里区分,而不是盲目地遵循特定案例的一般建议.
请注意,以下内容忽略了对象容器的问题以及面对容器内多个对象时要做什么.(它可以部分忽略,因为有些物体不适合装入容器.)
当我们将类拆分为两种类型时,整个问题变得更容易思考.课程师可以有两种不同的职责:
(R)发布语义(又称释放内存)
(C)提交语义(也就是刷新文件到磁盘)
如果我们以这种方式查看问题,那么我认为可以认为(R)语义不应该导致dtor的异常,因为有a)我们无法对它做什么和b)许多自由资源操作不甚至提供错误检查,例如.void
free(void* p);
具有(C)语义的对象,如需要成功刷新其数据的文件对象或在dtor中执行提交的("范围保护")数据库连接是不同类型的:我们可以对错误做些什么(在应用程序级别)我们真的不应该继续,好像什么也没发生.
如果我们遵循RAII路线并且允许在其中具有(C)语义的对象,那么我认为我们还必须允许这样的数字可以抛出的奇怪情况.因此,您不应该将这些对象放入容器中,并且terminate()
如果在另一个异常处于活动状态时抛出commit-dtor ,程序仍然可以执行.
关于错误处理(提交/回滚语义)和异常,Andrei Alexandrescu有一个很好的对话:C++ /声明控制流中的错误处理(在NDC 2014上举行)
在细节中,他解释了Folly库如何实现UncaughtExceptionCounter
其ScopeGuard
工具.
(我应该注意到其他人也有类似的想法.)
虽然谈话并不专注于从一个投掷者那里投掷,但它展示了一种工具,可以在今天使用它来摆脱何时投掷的问题.
在将来,可能会有一个标准功能,请参阅N3614,并讨论它.
Upd '17:C++ 17标准功能就是这个std::uncaught_exceptions
.我会快速引用cppref文章:
笔记
使用
int
-returning 的示例uncaught_exceptions
是......首先创建一个保护对象,并在其构造函数中记录未捕获的异常的数量.输出由guard对象的析构函数执行,除非foo()抛出(在这种情况下,析构函数中未捕获的异常的数量大于构造函数观察到的数量)
问自己关于从析构函数中抛出的真正问题是"调用者可以对此做些什么?" 实际上是否有任何有用的异常,可以抵消从析构函数中抛出的危险?
如果我销毁一个Foo
对象,并且Foo
析构函数抛出一个异常,我可以合理地做些什么呢?我可以记录它,或者我可以忽略它.就这样.我无法"修复"它,因为Foo
对象已经消失了.最好的情况,我记录异常并继续,好像什么也没发生(或终止程序).这是否真的值得通过从析构函数抛出导致未定义的行为?
它很危险,但从可读性/代码可理解性的角度来看也没有意义.
你需要问的是这种情况
int foo() { Object o; // As foo exits, o's destructor is called }
什么应该抓住异常?应该是foo的来电者吗?或者应该foo处理它?foo的调用者为什么要关心foo内部的某个对象?可能有一种方式语言定义这是有道理的,但它将是不可读的和难以理解.
更重要的是,Object的内存在哪里?对象所拥有的内存在哪里?它仍然被分配(表面上是因为析构函数失败了)?还要考虑对象是在堆栈空间中,所以它显然不管怎样.
然后考虑这种情况
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 } } };
另见此常见问题解答
来自C++的ISO草案(ISO/IEC JTC 1/SC 22 N 4411)
所以析构函数通常应该捕获异常,而不是让它们从析构函数中传播出来.
3在从try块到throw表达式的路径上构造的自动对象调用析构函数的过程称为"堆栈展开".[注意:如果在堆栈展开期间调用的析构函数以异常退出,则调用std :: terminate (15.5.1).所以析构函数通常应该捕获异常,而不是让它们从析构函数中传播出来. - 结束说明]
您的析构函数可能正在其他析构函数链中执行.抛出未被直接调用者捕获的异常会使多个对象处于不一致状态,从而导致更多问题,然后忽略清理操作中的错误.
其他人都解释了为什么抛出破坏者是可怕的......你能做些什么呢?如果您正在执行可能失败的操作,请创建一个单独的公共方法来执行清理并可以抛出任意异常.在大多数情况下,用户会忽略它.如果用户想要监视清理的成功/失败,他们可以简单地调用显式清理例程.
例如:
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... };
作为主要答案的补充,这些答案是好的,全面的和准确的,我想评论你引用的文章 - 那个说"在析构函数中抛出异常并不是那么糟糕"的文章.
本文采用了"抛出异常的替代方法"这一行,并列出了每种替代方案的一些问题.这样做就得出结论,因为我们找不到无问题的替代方案,所以我们应该继续抛出异常.
麻烦的是,它与替代品列出的任何问题都不会像异常行为那样糟糕,我们记得,这是"程序未定义的行为".一些作者的反对意见包括"审美丑陋"和"鼓励坏风格".那你想要哪个?一个风格不好的程序,或者表现出不确定行为的程序?