第8条:防止异常逃离析构函数(prevent exceptions from leaving destructors)

探讨C++中析构函数引发异常的风险,提出两种处理策略:终止程序或忽略异常,并建议通过改进接口设计使客户能够处理潜在问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 的析构函数可以:

1、如果 close 抛出异常则终止程序,通常通过调用 abort 实现
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
   // 在日志上记载:调用 close 失败 ;
   std::abort();
}
}
如果程序在析构过程中发生了一个错误而无法运行下去,上面的方法就是一个可行的选择。如果允许析构函数传播异常将导致程序行为的无法预知,调 用 abort 函 数可以 防止程序产生此类行为。

2、忽略这个异常 —— 由调用 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 会忽略这个异常或者终止程序。客户端程序员对此也没有什么好抱怨的,毕竟他们有机会第一手处理这问题但他们选择了放弃。

需要记住的
1、不要让析构函数引发异常。如果析构函数所调用的函数会抛出异常的话,析构函数应捕捉任何异常,然后忽略它们(不传播)或者终止程序。
2、如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应提供一个普通函数(而非在析构函数中)执行此操作。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值