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

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

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。

总结
  • 析构函数应该永不引发异常。如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后抑制它们或者终止程序。
  • 如果类客户需要能对一个操作抛出的异常做出回应,则那个类应该提供一个常规的函数(也就是说,非析构函数)来完成这个操作。