条款 29 :为“异常安全”而努力是值得的

条款 29 :为“异常安全”而努力是值得的

Strive for exception-safe code.

假设有一个class用来表现带背景的GUI菜单。希望其用于多线程环境,所以他得有一个互斥器作为并发控制之用。

class PrettyMenu{
public:
    ...
    void changeBackground(std::istream& imgSrc);//改变背景图片
    ...
private:
    Mutex mutex;//互斥器
    Image* bgImage;//目前得背景图像
    int imageChanges;//背景图像被改变得次数    
}

下面是changeBackground函数得可能实现:

void PrettyMenu::changeBackground(std::istream& imgSrc){
    lock(&mutex);//取得互斥器
    delete bgImage;//摆脱旧的背景图像
    ++imageChanges;//修改图像的更改次数
    bgImage = new Image(imgSrc);//放置新的图像
    unlock(&mutex);//释放互斥器
}

从“异常安全性”的角度来看,这个代码糟糕至极。

“异常安全”有两个条件,但是这个函数没有满足任意一个。这两个条件是,当异常被抛出时,有“异常安全”的函数就会:

  1. **不泄露任何资源。**上面的代码没有做到这一点,因为一旦new出现问题,对于unock()的调用就不会执行到,于是互斥器永远被锁住。
  2. **不允许数据败坏。**如果new出现异常,bgImage就指向了一个已经被删除的对象,imageChanges也已经被累加,但是实际上却没有改变。

解决资源泄漏的问题很容易,回想条款13和条款14.这里我们再说说Lock class:

Lock class用来管理互斥锁,的基本结构由RAII守则支配。也就是资源再构造期间获得,在析构期间释放:

class Lock{
public:
    explicit Lock(Muutex* pm):mutexPtr(pm){//获得资源
        lock(mutexPtr);
    }
    ~Lock(){unlock(mutexPtr);}//释放资源
private:
    Mutex* mutexPtr;  
}

使用Lock class 我们也就遵循了RAII守则,同时也解决上述糟糕代码的问题:

void PrettyMenu::changeBackground(std::istream& imgSrc){
    Lock ml(&mutex);//获得互斥器并确保其稍后被释放
    delete bgImage;//摆脱旧的背景图像
    ++imageChanges;//修改图像的更改次数
    bgImage = new Image(imgSrc);//放置新的图像
}

这里再次实践了以对象管理资源的好处。这里解决了资源泄漏的问题,现在我们可以专注数据败坏的问题了。在此之前我们先来看看一些基本术语:

**异常安全函数(Exception-safe functions)**提供以下三个保证之一:

  1. **基本承诺:**如果异常被抛出,程序内的任何事物仍然保持在有效状态下。所有对象内部处于一种前后一致的状态,然而程序的现实状态未可知。举个例子,例如上述new失败后,PrettyMenu类应该具有原背景图或被置为缺省的图,具体是那张图客户没有控制权。
  2. **强烈保证:**如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功则完全成功,否则程序会恢复到调用之前的状态。这比基本承诺更加简单直接,因为他保证程序只有两个状态,要么成功,要么不成功;而基本承诺在调用之后程序会处于任何状态(只要该状态合法)。
  3. **不抛弃保证:**承诺绝不抛出异常,因为他总能完成他们原先承诺的功能。作用于内置类型身上的所有操作都应该有nothrow保证。这是异常安全代码中一个必不可少的关键基础材料。

异常安全代码必须要提供上述保证之一。

一般你会想直接提供notrhrow保证,但是实际情况是一个函数很难不抛出异常,所以一般我们都在基本承诺和强烈保证之间选择。

这里让我们再实践以对象管理资源的忠告,来防止数据败坏,看下列代码:

class PrettyMenu{
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
}
void PrettyMenu::changeBackground(std::istream& imgSrc){
    Lock ml(&mutex);//获得互斥器并确保其稍后被释放
   	bgImage.reset(new Image(imgSrc));//以new Image的执行结果设定bgImage的内部指针
    ++imageChanges;//修改图像的更改次数
}   

这里,我们就不需要在手动delete旧图像了,因为这个动作被智能指针自动处理了。而且删除动作只有new成功之后才会发生,准确的说shared_ptr::reset函数只有其参数(也就是new Image的执行结果)被成功生成之后才会被调用。delete只在reset内被调用,如果未曾进入过该函数,那么也就不会调用delete了。

然而上述解决方法还只是提供了基本保证,因为如果Image构造函数如果出现异常,可能输入流的读取记号已经被移走。

下面给出提供强烈保证的方法:

我们首先了解一下Copy and Swap:为你打算修改的对象(原件)做出一个副本(copy),然后在那个副本中做一切必要的修改。若任何修改抛出异常,你的原件保持不变。待所有修改都成功之后,再将修改之后的副本的原对象在一个不抛出异常的操作中置换(swap)。

struct PMImpl{
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges;
};
class PrettyMenu{
    ...
private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
    using std::swap
    Lock ml(&mutex);//获得互斥器并确保其稍后被释放
    //获得mutex的副本数据
    std::tr1::shared_ptr<PMImpl> PNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));//修改副本
    ++pNew->imageChanges;
    swap(pImpl,pNew);//安全的交换 
}   

不存在在局部异常安全系统,作者给系统安全性打了一个比方:一个女生怀孕了就是怀孕了,没有就是没有,不能说部分怀孕。同样也不存在系统局部是安全的这么一说。

请记住

  1. 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型,强烈型,不抛出异常型。

  2. “强烈保证 ”往往能够以copy and swap实现出来,但是强烈保证并不是对所有函数都有可实现或具备实现意义。

  3. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值