大多数人说永远不要从析构函数抛出异常——这样做会导致未定义的行为。Stroustrup指出,“vector析构函数显式地为每个元素调用析构函数。这意味着如果元素析构函数抛出,则vector销毁失败…实际上没有很好的方法来防止析构函数抛出异常,因此标准库不保证元素析构函数是否抛出”(摘自附录E3.2)。
这篇文章似乎不是这么说的——抛出析构函数或多或少是可以的。
所以我的问题是,如果从析构函数抛出导致未定义的行为,你如何处理析构函数期间发生的错误?
如果在清理操作期间发生错误,您会忽略它吗?如果它是一个可以在堆栈中处理但不能在析构函数中处理的错误,那么从析构函数抛出异常难道没有意义吗?
显然,这种错误很少见,但也有可能发生。
我所在的小组认为,在析构函数中加入“作用域保护”模式在许多情况下都很有用——特别是对于单元测试。但是,要注意,在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()在抛出一个新的异常之前检查是否已经有一个异常。
这很危险,但从可读性/代码可理解性的角度来看,这也没有意义。
你要问的是在这种情况下
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
在这里,我们必须有所区分,而不是盲目地按照具体情况的一般建议行事。
请注意,下面的内容忽略了对象容器的问题,以及在容器内存在多个对象的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()抛出(在这种情况下,未捕获的数量
析构函数中的异常大于构造函数中的异常
观察到)