大多数人说永远不要从析构函数抛出异常——这样做会导致未定义的行为。Stroustrup指出,“vector析构函数显式地为每个元素调用析构函数。这意味着如果元素析构函数抛出,则vector销毁失败…实际上没有很好的方法来防止析构函数抛出异常,因此标准库不保证元素析构函数是否抛出”(摘自附录E3.2)。
这篇文章似乎不是这么说的——抛出析构函数或多或少是可以的。
所以我的问题是,如果从析构函数抛出导致未定义的行为,你如何处理析构函数期间发生的错误?
如果在清理操作期间发生错误,您会忽略它吗?如果它是一个可以在堆栈中处理但不能在析构函数中处理的错误,那么从析构函数抛出异常难道没有意义吗?
显然,这种错误很少见,但也有可能发生。
在这里,我们必须有所区分,而不是盲目地按照具体情况的一般建议行事。
请注意,下面的内容忽略了对象容器的问题,以及在容器内存在多个对象的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()抛出(在这种情况下,未捕获的数量
析构函数中的异常大于构造函数中的异常
观察到)
从析构函数抛出异常是危险的。
如果另一个异常已经在传播,则应用程序将终止。
#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:防止异常离开析构函数
在这里,我们必须有所区分,而不是盲目地按照具体情况的一般建议行事。
请注意,下面的内容忽略了对象容器的问题,以及在容器内存在多个对象的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()抛出(在这种情况下,未捕获的数量
析构函数中的异常大于构造函数中的异常
观察到)
我目前遵循的策略(很多人都这么说)是,类不应该主动从析构函数抛出异常,而是应该提供一个公共的“关闭”方法来执行可能失败的操作……
...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.
关键是容器保持中立,由所包含的类决定它们是否从析构函数抛出异常。