结论:
1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该能够捕捉任何异常,然后吞下他们(不传播)或结束程序。
2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而不是在析构函数中)执行该操作。
C++ 不禁止但不鼓励从析构函数引发异常。考虑:
class Widget {
public:
...
~Widget() { ... } // 假设这里可能吐出一个异常
};
void doSomething()
{
std::vector<Widget> v;
...
} // v在这里被自动销毁
当 vector v 被析构时,它有责任析构它包含的所有 Widgets。但假设在那些调用期间,先后有两个Widgets抛出异常,对于 C++ 来说,这太多了。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。在本例中将导致不明确行为,使用标准库的任何其他容器(如list,set)或TR1的任何容器甚至array,也会出现相同情况。C++ 不喜欢析构函数吐出异常。
如果你的析构函数需要执行一个可能失败而抛出一个异常的操作,该怎么办呢?假设使用一个class负责数据库连接,为了确保客户不会忘记在 DBconnection对象上调用 close(),一个合理的想法是创建一个用来管理DBConnection资源的类,并在其析构函数中调用close:
class DBConn { // 这个类用来管理DBConnection对象
public: // objects
...
~DBConn() // 确保数据库连接总是会被关闭
{
db.close();}
private:
DBConnection db;
};
它允许客户像这样编程:
{ // 打开一个区块(block)
DBConn dbc(DBConnection::create());
// 建立DBConnection并交给DBConn对象以便管理
... // 通过DBConn的接口使用DBConnection对象
} //在区块结束点,DBConn对象被销毁,因而自动为DBConnection对象调用close
只要调用 close 成功,一切都美好。但是如果这个调用导致一个异常,DBConn 的析构函数将传播那个异常,也就是允许它离开析构函数。这就产生了问题,因为析构函数抛出了一个烫手的山芋。
有两个主要的方法避免这个麻烦:
(1)Terminatethe program:如果 close 抛出异常就终止程序,一般是通过调用 abort。
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
制作运转记录,记下对close的调用失败;
std::abort();
}
}
它有一个好处是:阻止异常从析构函数中传播出去(那会导致不明确的行为)。也就是说,调用 abort 可以预先制“不明确行为”于死地。
(2)Swallowthe exception:吞下因调用close而发生的异常。在此例中将在第一种方法下去掉abort那句语句。
通常,将异常吞掉是个坏主意,因为它隐瞒了“某些动作失败”的重要信息!然而,有些时候,吞下异常比冒程序过早终止或不明确行为的风险更可取。程序必须能够在遭遇到一个错误并忽略之后还能继续可靠地运行,这才能成为一个可行的选择。
以上方法的问题都在于两者无法对引起 close 抛出异常的情况做出回应。
一个更好的策略是重新设计 DBConn 的接口,以使客户有机会对可能发生的问题做出回应。
class DBConn {
public:
...
void close() // 供客户使用的新函数
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try {
db.close(); // 关闭连接(如果客户不那么做的话)
}
catch (...) { // 如果关闭动作失败,记录下来并结束程序或吞下异常
制作运转记录,记下对close的调用失败;
...
}
}
}
private:
DBConnection db;
bool closed;
};
这样把调用 close 的责任从 DBConn 的析构函数移交给 DBConn 的客户(同时在 DBConn 的析构函数中仍内含一个“双保险调用”)。如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。这是因为析构函数)引发异常是危险的,永远都要冒着程序过早终止或 不明确行为的风险。在本例中,让客户自己调用 close 并不是强加给他们的负担,而是给他们一个处理错误的机会。他们可以忽略它,依靠 DBConn 的析构函数去调用 close。如果真有错误发生,close的确抛出异常而且DBConn吞下该异常或结束程序,客户没有立场抱怨,毕竟他们曾有机会第一手处理问题,而他们选择了放弃。