洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目8] 防止因异常中止析构函数

C++并没有禁止析构函数引发异常,但是C++无疑不会推荐这一做法。这样做有充足的理由。请看下边的代码:

class Widget {

public:

  ...

  ~Widget() { ... }                // 假设它会引发一个异常

};

 

void doSomething()

{

  std::vector<Widget> v;

  ...

}                                  // v在这里被自动销毁

vector v被销毁时,它也有责任销毁其所包含的所有的Widget。假设v中包含十个Widget,并且在对第一个进行析构时抛出了一个异常。那么剩下的九个Widget则仍需要得到销毁(否则它们所占有的资源就会发生泄漏),所以v应该为所有剩下的Widget——调用析构函数。但是假设在对这些对象进行销毁时,又出现了第二个Widget抛出了一个异常,现在同时存在着两个活动的异常,这对于C++来说已经是太多了。在极端巧合的情形下,程序中同时出现了两个活动的异常,此时程序的运行要么会中止,要么会产生未定义行为。本示例将产生未定义行为。在使用其它的标准库容器(比如listset等),任意的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的析构函数可以:

终止程序——如果close抛出异常则终止程序通常通过调用abort实现:

DBConn::~DBConn()

{

 try { db.close(); }

 catch (...) {

   在日志上记载:调用close失败;

   std::abort();

 }

}

如果在析构过程中发生了一个错误,从而程序无法继续运行,上面的方法就是一个可行的选择。如果允许析构函数传播异常将导致程序产生未定义行为,这样做的优势就在于可以避免类似的事情发生。也就是说,调用abort函数可以防止程序产生未定义行为。

忽略这个异常——由调用close函数产生的异常

DBConn::~DBConn()

{

  try { db.close(); }

  catch (...) {

      在日志上记载:调用close失败;

  }

}

在大多数情况下,忽略掉异常的存在并不是一个好主意,因为这样做你会错过一些重要的信息——一些东西出错了!然而在某些时刻,忽略异常比让程序担上不成熟终止或未定义行为的风险要强一些。为了让忽略异常成为一个可行的方案,程序必须有能力在忽略刚发生的错误之后仍然可以稳定地继续运行。

这两个方案都不是那么的动人心弦。它们存在着同样的问题,即二者都没有办法在第一时间对close抛出的异常作出反应。

一个更好的策略是:改进DBConn接口的设计,使得客户有机会自己处理可能发生的问题。举例说,DBConn可以自己提供一个close函数,这样就为客户提供了途径来处理由DBConnectionclose产生的异常。这样做还可以跟踪DBConnection所建立的连接是否被DBConnection自己的close函数正常关闭,如果关闭失败则在DBConn的析构函数将其关闭。这可以防止已建立的连接发生泄漏。然而,如果在DBConn的析构函数中对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会忽略这个异常或者终止程序。客户对此也没有什么好抱怨的,毕竟,在处理问题时是他们犯下了第一个错误,是他们自己选择不去利用它的。

时刻牢记

永远不要让析构函数引发异常。如果析构函数所调用的函数会抛出异常的话,那么析构函数中要捕捉到所有异常,然后忽略它们或者终止程序。

在一次操作中,如果类的客户有必要对抛出的异常做出反应,那么这个类应该提供一个常规的函数(而不是析构函数)来进行这一操作。

posted on 2007-04-22 14:00 ★ROY★ 阅读(1411) 评论(3)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【翻译】Effective C++ (第8条:防止因异常而中止析构函数)  回复  更多评论   

我支持你!
2007-04-27 10:06 | sandy

# re: 【翻译】Effective C++ (第8条:防止因异常而中止析构函数)  回复  更多评论   

谢谢啊,这两天装系统总出问题,脑袋都装大了。。
2007-04-27 10:51 | ★田德健★

# re: 【翻译】Effective C++ (第8条:防止因异常而中止析构函数)  回复  更多评论   

千万不要冲动~装系统是最让我郁闷的事~我曾经深受其害!!
2007-05-05 21:54 | sandy

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理