条款08:别让异常逃离析构函数

127 篇文章 7 订阅
39 篇文章 3 订阅

条款08:别让异常逃离析构函数

Prevent exceptions from leaving destructuors

多个异常的抛出

在C++中,并不禁止析构函数吐出异常,但是它并不鼓励这样做。举个例子:

class Widget {
public:
    ...
    ~Widget() { ... }   //假设这段代码会吐出一个异常
};

void doSomething()
{
    std::vector<Widget> v;
    ...
}                       //v在这里被自动销毁

在上面的代码中,当vector v被销毁,它将会负责销毁其内的所有Widgets

假设v内含有是个Widgets,而在析构第一个元素期间,有一个异常被抛出。而其他的九个Widget还是应该被销毁,否则他们保存的资源将会发生泄漏,因此v应该调用它们的析构函数。
但是,在第二个析构,又有一个异常被抛出!这对于C++来说,异常太多了。

在这个例子里,容器或者array并不是这种问题的必要条件。只要析构函数吐出异常,即使使用的并非是容器或者array,程序也可能出现过早结束或者出现不明确的行为。

因此,C++并不希望析构函数吐出异常。

传播异常

但是,假设析构函数必须执行一个操作,而该动作却可能会在失败时抛出异常,这时则如何去做?
举个例子,设计一个class负责数据库的连接:

class DBConn {   //该class负责管理DBConnection对象
public:
    ...
    ~DBConn()     //确保数据库连接总是会被关闭
    {
        db.close();
    }
private:
    DBConnection db;
};

在使用的过程中:

{
//开启一个区块(block),建立DBConnection对象
//并交给DBConn对象以便管理。
//通过DBConn接口使用DBConnection对象。
//在区块结束点,DBConn对象会被销毁,
//因而自动为DBConnection对象调用close

DBConn dbc(DBConnection""create());
...
}

在上面的例子中,如果调用close成功,则程序并不会出现什么问题。但是,如果该调用导致了异常,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函数,因而赋予了用户一个机会得以处理“因该操作而发生的异常”。同时,DBConn也可以追踪所管理的DBConnection是否已经被关闭,并在答案为否的情况下调用析构函数将其关闭。
然而,如果DBConnection析构函数调用close失败,则又会回到“强迫结束程序”或“吞下异常”的老路:

class DBConn {
public:
    ...
    void close()      //供用户使用的新函数
    {
        db.close();
        closed = true;
    }
    ~DBConn()
    {
        if(!closed) {
            try { db.closed(); }   //关闭连接(如果用户不这么做的)
            catch ( ... ) {       //如果关闭动作失败,则记录下来并结束程序或吞下异常
                制作运转记录,记下对close的调用失败;
                ...
            }
        }
}
private:
    DBConnection db;
    bool closed;
};

在上面的例子可以得到:

  • 如果某个操作可能在失败时抛出异常,而又必须要去处理该异常,则这个异常必须来自析构函数以外的某个函数。

因为析构函数吐出异常,会带来“过早结束程序”或“发生不明确行为”的风险。

最后:

1,析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该应该捕捉任何异常,然后吞下它们(不传播)或者结束程序。

2,如果用户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值