| 订阅 | 在线投稿
分享
 
 
 

C++箴言:防止异常离开析构函数

来源:互联网  宽屏版  评论
2008-06-01 01:58:12

C++ 并不禁止从析构函数中引发异常,但是这确实妨碍了实践。至于有什么好的理由,考虑:

class Widget {

public:

...

~Widget() { ... } // assume this might emit an exception

};

void doSomething()

{

std::vector v;

...

} // v is automatically destroyed here

当 vector v 被析构时,它有责任销毁它包含的所有 Widgets。假设 v 中有十个 Widgets,在销毁第一个的时候,抛出一个异常。其他 9个 Widgets 仍然必须被销毁(否则他们持有的任何资源将被泄漏),所以 v 应该调用它们的析构函数。但是假设在这个调用期间,第二个 Widgets 的析构函数又抛出一个异常。现在有两个异常同时在活动中,对于 C++ 来说这太多了。在非常巧合的条件下发生这样两个同时活动的异常,程序的执行会终止或者引发未定义行为。在本例中,将引发未定义行为。与此相同,使用任何标准库容器(比如,list,set),任何 TR1中的容器,甚至是一个数组,都可能会引发未定义问题。并非必须是容器或数组才会陷入麻烦。程序夭折或未定义行为是析构函数引发异常的结果,即使没有使用容器或数组也会如此。C++ 不喜欢引发异常的析构函数。 这比较轻易理解,但是假如你的析构函数需要执行一个可能失败而抛出异常的操作,该怎么办呢?例如,假设你与一个数据库连接类一起工作:

class DBConnection {

public:

...

static DBConnection create(); // function to return

// DBConnection objects; params

// omitted for simplicity

void close(); // close connection; throw an

}; // exception if closing fails

C++箴言:防止异常离开析构函数
更多内容请看C/C++技术专题专题,或

为了确保客户不会忘记调用 DBconnection 对象的 close,一个合理的主意是为 DBConnection 建立一个资源治理类,在它的析构函数中调用 close。这样的资源治理类将在以后的文章中探讨,但在这里,只要认为这样一个类的析构函数看起来像这样就足够了:

class DBConn { // class to manage DBConnection

public: // objects

...

~DBConn() // make sure database connections

{ // are always closed

db.close();

}

PRivate:

DBConnection db;

};

它答应客户像这样编程:

{

// open a block

DBConn dbc(DBConnection::create()); // create DBConnection object

// and turn it over to a DBConn

// object to manage

... // use the DBConnection object

// via the DBConn interface

} // at end of block, the DBConn

// object is destroyed, thus

// automatically calling close on

// the DBConnection object

既然能成功地调用 close 那就好了,但是假如这个调用导致了异常,DBConn 的析构函数将散播那个异常,也就是说,它将离开析构函数。这就产生了问题,因为析构函数抛出了一个烫手的山芋。

C++箴言:防止异常离开析构函数
更多内容请看C/C++技术专题专题,或

有两个主要的方法避免这个麻烦。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 的接口,以使它的客户有机会对可能会发生的问题做出回应。例如,DBConn 能够自己提供一个 close 函数,从而给客户一个机会去处理从那个操作中发出的异常。它还能保持对它的 DBConnection 是否已被关闭的跟踪,假如没有关闭就在析构函数中自己关闭它。这样可以防止连接被泄漏。假如在 DBConnection 的析构函数中调用 close 失败,无论如何,我们还可以再返回到终止或者抑制。

class DBConn {

public:

...

void close() // new function for

{

// client use

db.close();

closed = true;

}

~DBConn()

{

if (!closed) {

try { // close the connection

db.close(); // if the client didn’t

}

catch (...) { // if closing fails,

make log entry that call to close failed; // note that and

... // terminate or swallow

}

}

private:

DBConnection db;

bool closed;

};

将调用 close 的责任从 DBConn 的析构函数转移到 DBConn 的客户(同时在 DBConn 的析构函数中包含一个“候补”调用)可能会作为一种肆无忌惮地推卸责任的做法而刺激你。你甚至可以把它看作一个忠告(使接口易于正确使用)的违反。实际上,这都不正确。假如一个操作可能失败而抛出一个异常,而且可能是一个需要处理的异常,这个异常就必须来自非析构函数。这是因为析构函数引发异常是危险的,永远都要冒着程序夭折或未定义行为的风险。在此例中,让客户调用 close 并不是强加给他们的负担,而是给他们一个时机去应付错误,否则他们将没有机会做出回应。假如他们找不到可用到机会(或许因为他们相信不会有错误真的发生),他们可能忽略它,依靠 DBConn 的析构函数为他们调用 close。假如一个错误恰恰发生在那时——假如由 close 抛出——假如 DBConn 抑制了那个异常或者终止了程序,他们将无处诉苦。究竟,他们无处着手处理问题,他们将不再使用它。

Things to Remember

·析构函数应该永不引发异常。假如析构函数调用了可能抛出异常的函数,析构函数应该捕捉任何异常,然后抑制它们或者终止程序。

·假如类客户需要能对一个操作抛出的异常做出回应,则那个类应该提供一个常规的(非析构函数)函数来完成这个操作。

C++箴言:防止异常离开析构函数
更多内容请看C/C++技术专题专题,或

  C++ 并不禁止从析构函数中引发异常,但是这确实妨碍了实践。至于有什么好的理由,考虑:      class Widget {    public:     ...     ~Widget() { ... } // assume this might emit an exception   };      void doSomething()   {    std::vector v;    ...   } // v is automatically destroyed here   当 vector v 被析构时,它有责任销毁它包含的所有 Widgets。假设 v 中有十个 Widgets,在销毁第一个的时候,抛出一个异常。其他 9个 Widgets 仍然必须被销毁(否则他们持有的任何资源将被泄漏),所以 v 应该调用它们的析构函数。但是假设在这个调用期间,第二个 Widgets 的析构函数又抛出一个异常。现在有两个异常同时在活动中,对于 C++ 来说这太多了。在非常巧合的条件下发生这样两个同时活动的异常,程序的执行会终止或者引发未定义行为。在本例中,将引发未定义行为。与此相同,使用任何标准库容器(比如,list,set),任何 TR1中的容器,甚至是一个数组,都可能会引发未定义问题。并非必须是容器或数组才会陷入麻烦。程序夭折或未定义行为是析构函数引发异常的结果,即使没有使用容器或数组也会如此。C++ 不喜欢引发异常的析构函数。 这比较轻易理解,但是假如你的析构函数需要执行一个可能失败而抛出异常的操作,该怎么办呢?例如,假设你与一个数据库连接类一起工作:      class DBConnection {    public:     ...         static DBConnection create(); // function to return     // DBConnection objects; params     // omitted for simplicity   void close(); // close connection; throw an   }; // exception if closing fails    [url=http://www.wangchao.net.cn/bbsdetail_1785103.html][img]http://image.wangchao.net.cn/it/1323424700331.gif[/img][/url] 更多内容请看C/C++技术专题专题,或   为了确保客户不会忘记调用 DBconnection 对象的 close,一个合理的主意是为 DBConnection 建立一个资源治理类,在它的析构函数中调用 close。这样的资源治理类将在以后的文章中探讨,但在这里,只要认为这样一个类的析构函数看起来像这样就足够了:      class DBConn { // class to manage DBConnection    public: // objects     ...     ~DBConn() // make sure database connections     { // are always closed      db.close();     }    PRivate:     DBConnection db;   };   它答应客户像这样编程:      {    // open a block    DBConn dbc(DBConnection::create()); // create DBConnection object    // and turn it over to a DBConn    // object to manage    ... // use the DBConnection object    // via the DBConn interface   } // at end of block, the DBConn   // object is destroyed, thus   // automatically calling close on   // the DBConnection object   既然能成功地调用 close 那就好了,但是假如这个调用导致了异常,DBConn 的析构函数将散播那个异常,也就是说,它将离开析构函数。这就产生了问题,因为析构函数抛出了一个烫手的山芋。 [url=http://www.wangchao.net.cn/bbsdetail_1785103.html][img]http://image.wangchao.net.cn/it/1323424700374.gif[/img][/url] 更多内容请看C/C++技术专题专题,或   有两个主要的方法避免这个麻烦。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 的接口,以使它的客户有机会对可能会发生的问题做出回应。例如,DBConn 能够自己提供一个 close 函数,从而给客户一个机会去处理从那个操作中发出的异常。它还能保持对它的 DBConnection 是否已被关闭的跟踪,假如没有关闭就在析构函数中自己关闭它。这样可以防止连接被泄漏。假如在 DBConnection 的析构函数中调用 close 失败,无论如何,我们还可以再返回到终止或者抑制。      class DBConn {   public:   ...      void close() // new function for   {    // client use    db.close();    closed = true;   }      ~DBConn()   {    if (!closed) {     try { // close the connection      db.close(); // if the client didn’t     }     catch (...) { // if closing fails,      make log entry that call to close failed; // note that and      ... // terminate or swallow     }    }       private:     DBConnection db;     bool closed;   };   将调用 close 的责任从 DBConn 的析构函数转移到 DBConn 的客户(同时在 DBConn 的析构函数中包含一个“候补”调用)可能会作为一种肆无忌惮地推卸责任的做法而刺激你。你甚至可以把它看作一个忠告(使接口易于正确使用)的违反。实际上,这都不正确。假如一个操作可能失败而抛出一个异常,而且可能是一个需要处理的异常,这个异常就必须来自非析构函数。这是因为析构函数引发异常是危险的,永远都要冒着程序夭折或未定义行为的风险。在此例中,让客户调用 close 并不是强加给他们的负担,而是给他们一个时机去应付错误,否则他们将没有机会做出回应。假如他们找不到可用到机会(或许因为他们相信不会有错误真的发生),他们可能忽略它,依靠 DBConn 的析构函数为他们调用 close。假如一个错误恰恰发生在那时——假如由 close 抛出——假如 DBConn 抑制了那个异常或者终止了程序,他们将无处诉苦。究竟,他们无处着手处理问题,他们将不再使用它。   Things to Remember   ·析构函数应该永不引发异常。假如析构函数调用了可能抛出异常的函数,析构函数应该捕捉任何异常,然后抑制它们或者终止程序。   ·假如类客户需要能对一个操作抛出的异常做出回应,则那个类应该提供一个常规的(非析构函数)函数来完成这个操作。 [url=http://www.wangchao.net.cn/bbsdetail_1785103.html][img]http://image.wangchao.net.cn/it/1323424700391.gif[/img][/url] 更多内容请看C/C++技术专题专题,或
󰈣󰈤
 
 
 
>>返回首页<<
 
 热帖排行
 
 
王朝网络微信公众号
微信扫码关注本站公众号wangchaonetcn
 
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
©2005- 王朝网络 版权所有