[转]http://www.cppblog.com/tiandejian/archive/2007/04/22/ecpp_08.html
第8条: 防止因异常而中止析构函数
C++ 并 没有禁止析构函数引发异常,但 是 C++ 十 分不推荐这一做法。这样做有充足的理由。请看下边的代码:
class Widget {
public:
...
~Widget() { ... } // 假设它会引发一个异常
};
void doSomething()
{
std::vector<Widget> v;
...
} // v 在这里被自动销毁
当 vector v 被销毁时,它也有责任销毁其所包含的所有的 Widget 。假设 v 中包含十个 Widget ,并且在对第一个进行 析构时抛出了一个异常。那么剩下的九 个 Widget 则仍需要得到销毁(否则它们所占有的资源就会发生泄漏),所以 v 应该为所有剩下的 Widget 一一调用析构函数。但是假设在对这些对象进行销毁时,又出现了第二个 Widget 抛 出了一个异常,现在同时存在着两个活动的异常,这对 于 C++ 来说 已经是太多了。在极端巧合的情形下,程序中同时出现了两个活动的异常,此时程序的运行要么会中止,要么会产生无法预知的行为。本示例将产生无法预知的行为。在使用其它的标准库容器(比 如 list 、 set 等),任意的 TR1 容 器(参见 第 54 条 ),甚至是一个数组,同样都会产生无法预知的行为。然而为你带来麻烦的不仅仅是这些容器或者数组,析构函数抛出异常会引发不成熟的程序终止或者未知的行为,甚至在没有容器和数组的情况下也会发生。 C++ 不喜欢能够引发异常的析构函数!
这个问题很好理解,但是当你的析构函数的某一操作可能失败,并且有可能对这一失败的操作抛出一个异常时,你应该怎么做呢?请看下边的示例,其中假设你使用一个类进行数据库连接:
class DBConnection {
public:
...
static DBConnection create(); // 返回 DBConnection 对象的函数;
// 为简化代码省略了参数表
void close(); // 关闭连接;若关闭失败则抛出异常
};
为了确保客户端程序员不会忘记 为 DBConnection 对象调 用 close 函数,一个可行的方案是:创建一个新的类来管理 DBConnection 的资源,在这个类的析构函数中调用 close 。这种资源管理类在第三章中作详细的介绍,在本节中,我们仅关心这些类的析构函数是什么样的:
class DBConn { // 该类用来管理 DBConnection 对象的资源
public:
...
~DBConn() // 确保数据库连接总能关闭
{
db.close();
}
private:
DBConnection db;
};
客户端程序员可以这样编写:
{ // 开始一个程序块
DBConn dbc(DBConnection::create());
// 创建一个 DBConnection 对象,然后
// 把它交给一个 DBConn 对象来管理
... // 通过 DBConn 的接口使用这个
// DBConnection 对象
} // 在该程序块的最后,这个 DBConn 对象
// 被销毁了,就好像自动调用了那个
// DBConnection 对象的 close 函数
只要对 close 的调用能成功,这个 DBConn 的方案就是一个好主意,但是一旦这一调用会引发一个异常, DBConn 的析构函数则会使这个异常蔓延开来,也就是所谓的,允许“因异常而中止析构函数”。这便是问题所在,因为析构函数在此处抛出异常意味着麻烦将会出现。
避免这类麻烦有两种主要的办法。 DBConn 的析构函数可以:
l 如果 close 抛出异常则终止程序,通常通过调用 abort 实现 :
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
在日志上记载:调用 close 失败 ;
std::abort();
}
}
如果在析构过程中发生了一个错误,从而程序无法运行下去了,上面的方法就是一个可行的选择。如果允许析构函数传播异常将导致程序行为无法预知,这样做的优势就在于可以避免类似的事情发生。也就是说,调 用 abort 函 数可以 防止程序产生未知的行为。
l 忽略这个异常 —— 由调用 close 函数产生的异常
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
在日志上记载:调用 close 失败 ;
}
}
在大多数情况下,忽略掉异常的存在并不是一个好主意,因为这样做你会错过一些重要的信息——一些东西出错了!然而在某些时刻,忽略异常比让程序担上不成熟终止或未知行为的风险要强一些。为了让忽略异常成为一个可行的方案,程序必须有能力在发生的错误被忽略之后仍然可以稳定地继续运行。
这两个方案都不是那么的动人心弦。这两者存在着同样的问题,它们没有办法在第一时间对 close 抛出的异常作出反应。
一个更好的策略是:改进 DBConn 接口的设计,使得客户端程序员有机会自己处理可能发生的问题。举例说, DBConn 可以自己包含一个 close 函数,这样就为客户端程序员提供了途径来处理由 DBConnection 的 close 产生的异常。这样做还可以保持跟踪 DBConnection 所建立的连接是否被 DBConnection 自己的 close 函数正常关闭,如果关闭失败则在 DBConn 的析构函数再次尝试。这可以防止已建立的连接发生泄漏。然而,如果在 DBConn 的析构函数 [1] 中对 close 的调用仍然不成功,我们还是需要中止运行或者忽略异常。
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 的析构函数还包含一个“备用的”调用)。可能你会认为这样做实属毫无顾忌地推卸责任,你甚至可能认为这是对“让接口更简单易用”这一忠告(见第 18 条)的违背。实际上,两者都不是。如果一个操作可能由于一次异常的抛出而失败,同时这个异常有需要得到处理,这一异常不应该来自析构函数。这是因为引发异常的析构函数是十分危险的,它使你的程序始终位于风口浪尖:你无法避免不成熟的终止和未知行为。在上边的示例中,让客户端程序员自己手动调用 close 并不会为其带来过多的负担,相反地,这样做为客户端程序员提供了处理错误的机会,否则他们就会束手无策。如果他们没有发现这个机会的裨益所在(可能是因为他们相信错误不会发生得这么巧),他们可以忽略这个机会,然后依赖 DBConn 的析构函数为他们调用 close 函数。如果就在这一刻发生了错误——也就是说 close 确实抛出了异常—— DBConn 会忽略这个异常或者终止程序。客户端程序员对此也没有什么好抱怨的,毕竟,在处理问题时是他们犯下了第一个错误,是他们自己选择不去利用它的。
需要记住的
l 永远不要让析构函数引发异常。如果析构函数所调用的函数会抛出异常的话,那么析构函数中要捕捉到所有异常,然后忽略它们或者终止程序。
l 在一次操作中,如果一个类的使用者有能力对抛出异常作出反应,那么这个类应该提供一个常规的函数(而不是析构函数)来进行这一操作。
[1] 原文有 误( DBConnection 的析构函数),此处始终未涉及到 DBConnection 的析 构函数。——译注