大多数人说永远不要从析构函数抛出异常——这样做会导致未定义的行为。Stroustrup指出,“vector析构函数显式地为每个元素调用析构函数。这意味着如果元素析构函数抛出,则vector销毁失败…实际上没有很好的方法来防止析构函数抛出异常,因此标准库不保证元素析构函数是否抛出”(摘自附录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 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:防止异常离开析构函数
我所在的小组认为,在析构函数中加入“作用域保护”模式在许多情况下都很有用——特别是对于单元测试。但是,要注意,在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()在抛出一个新的异常之前检查是否已经有一个异常。
从析构函数抛出异常永远不会导致未定义的行为。
The problem of throwing exceptions out a destructor is that destructors of successfully created objects which scopes are leaving while handling an uncaught exception (it is after an exception object is created and until completion of a handler of the exception activation), are called by exception handling mechanism; and, If such additional exception from the destructor called while processing the uncaught exception interrupts handling the uncaught exception, it will cause calling std::terminate (the other case when std::exception is called is that an exception is not handled by any handler but this is as for any other function, regardless of whether or not it was a destructor).
如果正在处理未捕获的异常,您的代码永远不知道附加的异常是否会被捕获,或者是否会归档未捕获的异常处理机制,因此永远不知道抛出异常是否安全。
但是,有可能知道正在处理未捕获的异常(https://en.cppreference.com/w/cpp/error/uncaught_exception),因此可以通过检查条件来过度处理,只在情况不是这样的情况下抛出(在某些情况下它不会抛出,但它是安全的)。
但在实践中,将程序分成两种可能的行为并没有什么用——它只是无助于你设计出一个设计良好的程序。
If you throw out of destructors ignoring whether or not an uncaught exception handling is in progress, in order to avoid possible calling std::terminate, you must guarantee that all exceptions thrown during lifetime of an object that may throw an exception from their destructor are caught before beginning of destruction of the object.
It is quite limited usage; you hardly can use all classes which would be reasonably allowed to throw out of their destructor in this way; and a combination of allowing such exceptions only for some classes with such restricted usage of these classes impede making a well-designed program, too.