从析构函数中抛出异常
大多数人说永远不会从析构函数中抛出异常 - 这样做会导致未定义的行为。 Stroustrup指出:“向量析构函数明确地为每个元素调用析构函数,这意味着如果一个元素析构函数抛出,向量销毁失败......真的没有什么好办法来防止从析构函数中抛出异常,所以库如果元素析构函数抛出不作任何保证“(来自附录E3.2)。
这篇文章似乎另有说法 - 抛出析构函数或多或少都可以。
所以我的问题是 - 如果从析构函数中抛出导致未定义的行为,那么如何处理析构函数期间发生的错误?
如果在清理操作过程中发生错误,您是否忽略它? 如果它是一个可能在堆栈中处理的错误,但在析构函数中不正确,那么将异常抛出析构函数没有意义吗?
显然这些错误是罕见的,但可能的。
从析构函数中抛出异常是很危险的。
如果另一个异常已经传播,应用程序将终止。
#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 Thisn";
}
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 thisn";
}
}
这基本上归结为:
任何危险的(即可能抛出异常)都应该通过公共方法(不一定是直接)完成。 然后,您的类的用户可以通过使用公共方法并捕获任何潜在的异常来处理这些情况。
然后析构函数将通过调用这些方法来完成对象(如果用户没有明确这样做),但是任何异常抛出都会被捕获并丢弃(在尝试修复问题之后)。
所以实际上你将责任传递给用户。 如果用户能够纠正异常,他们将手动调用相应的功能并处理任何错误。 如果对象的用户不担心(因为对象将被销毁),那么析构函数被留下来处理业务。
一个例子:
的std :: fstream的
close()方法可能会引发异常。 如果文件已被打开,析构函数调用close(),但确保任何异常不会传播出析构函数。
因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,他们将手动调用close()并处理任何异常。 另一方面,如果他们不在乎,那么解析器将被留下来处理这种情况。
Scott Myers在他的书“Effective C ++”中有一篇关于该主题的优秀文章,
编辑:
显然,在“更有效的C ++”
项目11:防止异常离开析构函数
抛出析构函数可能会导致崩溃,因为此析构函数可能被称为“堆栈展开”的一部分。 堆栈展开是抛出异常时发生的过程。 在这个过程中,所有从“try”开始直到抛出异常被推入堆栈的对象将被终止 - >它们的析构函数将被调用。 在此过程中,不允许执行另一个异常抛出,因为一次不能处理两个异常,因此会引发abort()调用,程序将崩溃并且控件将返回到操作系统。
我们必须在此区分 ,而不是盲目地遵循针对具体情况的一般建议。
请注意,以下内容忽略了对象容器的问题以及面对容器内多个对象时要做什么。 (部分对象可能会被忽略,因为有些对象不适合放入容器。)
当我们以两种类型拆分类时,整个问题变得更容易思考。 一个类可以有两个不同的职责:
如果我们用这种方式来看待这个问题,那么我认为可以认为(R)语义不应该导致一个例外,因为存在这样一个例外:我们无法做到这一点,并且b)许多自由资源操作不甚至提供错误检查,例如void
free(void* p);
。
具有(C)语义的对象,例如需要成功刷新其数据的文件对象或在Dtor中执行提交的(“范围防护”)数据库连接,具有不同的类型:我们可以对错误进行操作应用程序级别),我们真的不应该继续,因为没有任何事情发生。
如果我们遵循RAII路线并且允许在它们的角色中具有(C)语义的对象,那么我认为我们还必须考虑到这种情况可能出现的奇怪情况。 由此可见,不应该将这些对象放入容器中,而且如果在另一个异常处于活动状态时抛出commit-dtor,程序仍然可以terminate()
。
关于错误处理(提交/回滚语义)和异常,Andrei Alexandrescu有一个很好的演讲:C ++ /声明控制流程中的错误处理(在NDC 2014举办)
在细节中,他解释了Folly库如何为ScopeGuard
工具实现UncaughtExceptionCounter
。
(我应该注意到其他人也有类似的想法。)
虽然谈话并不关注掷骰子,但它展示了一种工具,可以用来摆脱抛出时间的问题。
未来,这可能有一个标准功能,请参阅N3614,并对此进行讨论。
更新'17:C ++ 17标准功能是std::uncaught_exceptions
afaikt。 我会尽快引用cppref文章:
笔记
使用int
-returning uncaught_exceptions
的示例是......首先创建一个警戒对象,并在其构造函数中记录未捕获异常的数量。 输出由guard对象的析构函数执行,除非foo()抛出(在这种情况下,析构函数中的未捕获异常的数量大于构造函数观察到的数量)