「生活可以更简单, 欢迎来到我的开源世界」
  1. 析构函数中发生异常是件棘手的事
  2. 析构函数中处理异常的两种思路
  3. 提供类用户异常处理接口
  4. 总结
Item8-别让异常逃离析构函数
2021-01-01
」 「

Item8-别让异常逃离析构函数

析构函数中发生异常是件棘手的事

由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为

class Widget {
public:
...
~Widget() { ... } // assume this might emit an exception
};

void doSomething()
{
std::vector<Widget> v;
...
}

当 v 被析构时,它有责任析构它包含的所有 Widgets。假设 v 中有十个 Widgets,在第一个的析构过程中,抛出一个异常。其它 9 个 Widgets 仍然必须被析构,否则它们持有的所有资源将被泄漏。这时如果第二个 Widget 析构又抛出异常,现在有两个同时活动的异常,程序若不是结束执行就是引发未定义行为。

假设使用一个类负责数据库连接:

class DBConnection {
public:
...
static DBConnection create();
void close();
};

为了确保客户不会忘记在对象上调用 close,一个合理的主意是为 DBConnection 建立一个资源管理类,在它的析构函数中调用 close。

class DBConn {               
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};

使用时:

{                                      
DBConn dbc(DBConnection::create());
...
}

如果 DBConn 析构函数调用close导致异常,则析构函数就会传播该异常,也就是允许异常离开这个析构函数,这会造成麻烦。

析构函数中处理异常的两种思路

有两个办法可以避免这一问题,DBConn的析构函数可以:

如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项,毕竟它可以阻止异常从析构函数传播出去从而导致未定义行为。

一般而言,吞掉异常是个坏主意,因为它压制了“某些动作失败”的重要信息,然而有些时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

提供类用户异常处理接口

在遭遇并忽略了一个错误后,程序必须能够继续可靠地执行,这才是一个可行的方案。

一个极佳的策略是重新设计 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 并不是对他们带来负担,而是给他们一个处理错误的机会,否则他们没有机会响应。

总结

<⇧>