Effective C++——条款29(第5章)

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

Strive for exception-safe code

    假设有个 class 希望用于多线程环境,所以它有个互斥器(mutex)作为并发控制(concurrency control):
class PrettyMenu {
public:
    ...
    void changeBackground(std::istream& imgSrc);
    ...
private:
    Mutex mutex;                    // 互斥器
    Image* bgImgage;        
    int imageChanges;
};
    下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    lock(&mutex);                    // 取得互斥器,详见条款14
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);    
    unlock(&mutex);                    //释放互斥器
}
     从"异常安全性"的观点来看,这个函数很糟糕."异常安全性"有两个条件,而这个函数没有满足其中任何一个条件.
     当异常被抛出时,带有异常安全性的函数会:
     1.不泄露任何资源.上述代码并没有做到这点,因为一旦"new Image(imgSrc)"导致异常,对unlock的调用就不会执行,于是互斥器就永远被把持住了.
     2.不允许数据败坏.如果"new Image(imgSrc)"抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已经被累加,而其实新的图像并没有被成功安全.
     解决资源泄露的问题很容易,条款13讨论过如何以对象管理资源, 条款14也导入Lock class作为一种"确保互斥器被及时释放"的方法:
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    Lock ml(&mutex);        // 条款14:获得互斥器并确保它稍后被释放
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
}
    关于"资源管理类"如Lock,一个很棒的事情是,它们通常使函数更短.
    异常安全函数提供以下是三个保证之一:
    基本保证:如果异常被抛出,程序内的任何事物保持在有效状态下.没有任何对象或数据结果会因此而败坏,所有对象都处于一种内部前后一致的状态.
    强烈保证:如果异常被抛出,程序状态不改变.调用这样的函数需要有这样的认识:如果函数成功,就是完全成功,如果函数失败,程序会回复到"调用函数之前"的状态.
     不抛出(nothrow)保证:承诺不抛出异常,因为它们总是能够完成它们原先承诺的功能.作用于内置类型身上的所有操作都提供nothrow保证.这是异常安全码中一个必不可少的关键基础材料.
    异常安全码必须提供上述三种保证之一.因此抉择就是,该为所写的每一个函数提供哪一种保证.
    一般而言,应该提供最强烈保证,从异常安全性的观点看,nothrow函数很棒,但是很难在C part of C++领域中完全没有调用任何一个可能抛出异常的函数.任何使用动态内存的东西如果无法找到足够内存以满足需求,通常会抛出一个bad_alloc异常(详见条款49).因此,如果可能的话就提供nothrow保证,但对大部分函数而言,抉择往往落在基本保证和强烈保证之间.
    对changeBackground而言,提供强烈保证是可能的. 首先改变PrettyMenu的bgImage成员变量类型,从一个类型为Imgae* 的指针改为一个"用于资源管理"的智能指针(详见 条款13).这个构想只是帮助防止资源泄露.它对"强烈的异常安全保证"的帮助仅仅只是强化了 条款13的论点:以对象管理资源是良好设计的根本. 第二,重新排列语句次序.一般而言 这是个好策略:不要为了表示某件事情发生而改变对象状态,除非那件事情真的发生了.
    下面是结果:
class PrettyMenu {
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    Lock m1(&mutex);
    bgImage.reset(new Image(imgSrc));    // 以"new Image"的执行结果设定bgImage内部指针
    ++imageChanges;
}
    这里 不再需要手动 delete 旧图像,因为这个动作已经有智能指针内部完成了.此外,删除动作只发生在新图像被成功创建之后.更正确地说, tr1::shared_ptr::reset函数只有在其参数(也就是"new Image(imgSrc)"的执行结果)被成功生成后才会被调用.delete 只在reset函数内被使用,所以如果从未进入那个函数也就绝不会使用 delete.
    有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它.这个策略被称为 copy and swap.原则很简单: 为打算修改的对象(原本)做出一份副本,然后在副本上做一切必要修改.若有任何修改动作抛出异常,原对象仍保持未改变状态.待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap).
    "copy and copy"策略是对对象状态做出"全有或全无"改变的一个很好的办法
,但一般而言它并不保证整个函数有强烈的异常安全性. 因为有"连带影响",如果函数只操作局部性状态,便相对容易提供强烈保证.但是当函数对"非局部性数据"有连带影响时,提供强烈保证就困难的多.一般而言在"数据库修改动作"发出之后,没有什么做法可以取消并恢复数据库旧观,因为数据库的其他客户可能已经看到了这一笔数据.
    这些方面会阻止为函数提供强烈保证, 另一个方面是效率,copy and swap的关键在于"修改对象数据的副本,然后子啊一个不抛出异常的函数中将修改后的数据和原件置换",因此必须为每一个即将被改动的对象做出一个副本,那要耗费可能无法供应的时间和空间.
    当"强烈保证"不切实际时,就必须提供"基本保证".现实中或许会发现,可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使人退却.因此,对许多函数而言,"异常安全性的基本保证"是一个绝对合理的选择.
     当撰写新码或修改旧码时,请仔细想想如何让它具备异常安全性.首先是"以对象管理资源"(条款13),那可阻止资源泄露.然后是挑选三个"异常安全保证"中的某一个实施于所写的每一个函数上.最后将决定写成文档.
    注意:
    异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏.这样的函数区分为三种可能的保证:基本保证,强烈保证,不抛出异常.
    "强烈保证"往往能够以copy and swap实现出来,但"强烈保证"并非对所有函数都可实现或具备现实意义.
    函数提供的"异常安全保证"通常最高只等于其所调用的各个函数的"异常安全保证"中的最弱者.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值