Item 8:析构函数不要抛出异常
析构函数中发生异常是件棘手的事
由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为
class Widget {
public:
...
~Widget() { ... } // assume this might emit an exception
};
void doSomething()
{
std::vector<Widget> v;
...
}
当 v 被析构时,它有责任析构它包含的所有 Widgets。假设 v 中有十个 Widgets,在第一个的析构过程中,抛出一个异常。其它 9 个 Widgets 仍然必须被析构否则它们持有的所有资源将被泄漏。在非常巧合的条件下产生这样两个同时活动的异常,程序的执行会终止或者引发未定义行为。
假设你与一个数据库连接类一起工作:
class DBConnection {
public:
...
static DBConnection create();
void close();
};
为了确保客户不会忘记在对象上调用 close,一个合理的主意是为 DBConnection 建立一个资源管理类,在它的析构函数中调用 close。
class DBConn {
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
使用时:
{
DBConn dbc(DBConnection::create());
...
}
析构函数中处理异常的两种思路
只要能成功地调用 close 就可以了,但是如果这个调用导致一个异常,DBConn 的析构函数将传播那个异常,也就是说,它将离开析构函数。有两个主要的方法避免这个麻烦。
- 如果 close 抛出异常就终止程序,一般是通过调用 abort:
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
make log entry that the call to close failed;
std::abort();
}
}
如果在析构的过程遭遇到错误后程序不能继续运行,这就是一个合理的选择。调用 abort 就可以预先防止未定义行为。
- 抑制这个对 close 的调用造成的异常
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
make log entry that the call to close failed;
}
}
提供类用户异常处理接口
通常,抑制异常是一个不好的主意,因为它会隐瞒重要的信息—— 某事失败了!然而,有些时候,抑制异常比冒程序过早终止或未定义行为的风险更可取。程序必须能够在遭遇到一个错误并忽略之后还能继续可靠地运行,这才能成为一个可行的选择。
这些方法都不太吸引人。它们的问题首先在于程序无法对引起 close 抛出异常的条件做出回应。
一个更好的策略是设计 DBConn 的接口,以使它的客户有机会对可能会发生的问题做出回应。
class DBConn {
public:
...
void close() //@ new function for client use
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) {
try { //@ close the connection if the client didn't
db.close();
}
catch (...) { //@ if closing fails,note that and terminate or swallow
make log entry that call to close failed;
...
}
}
private:
DBConnection db;
bool closed;
};
将调用 close 的责任从析构函数移交给 DBConn 的客户,同时在 DBConn 的析构函数中包含一个“候补”调用。
让客户自己调用 close 并不是强加给他们的负担,而是给他们一个时机去应付错误,否则他们将没有机会做出回应。如果他们找不到可用到机会,他们可以忽略它,依靠 DBConn 的析构函数为他们调用 close。
总结
- 析构函数应该永不引发异常。如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后抑制它们或者终止程序。
- 如果类客户需要能对一个操作抛出的异常做出回应,则那个类应该提供一个常规的函数(也就是说,非析构函数)来完成这个操作。