Effective C++ 读书笔记 Item29 追求异常安全的代码

朱雀 2020-07-05 02:30 1034阅读 0赞

考虑下面例子,有一个菜单类, changeBg 函数可以改变它的背景,切换背景计数,同时提供线程安全:

  1. class Menu{
  2. Mutex mutex; //提供多线程互斥访问
  3. Image *bg; //背景图片
  4. int changeCount; //切换背景计数
  5. public :
  6. void changeBg(istream& sr);
  7. };
  8. void Menu::changeBg(istream& src){
  9. lock(&mutex);
  10. delete bg;
  11. ++changeCount;
  12. bg = new Image(src);
  13. unlock(&mutex);
  14. }

1)异常安全的 2 个条件
异常安全有 2 个条件:

  1. 不泄露任何资源:即发生异常时,异常发生之前获得的资源都应该释放,不会因为异常而泄露。在
    上面的例子中,如果 new Image 发生异常,那么 unlock 就不会调用,因此锁资源会泄露
  2. 不允许数据败坏:上面的例子也不符合,如果 new Image 异常, 背景图片会被删除,计数也会改变。
    但是新背景并未设置成功
    对于资源泄露, 条款 13 讨论过以对象管理资源。锁资源也可以为 shared_ptr 指定“删除器”,当引用为 0
    时,即异常发生,管理所资源的对象被销毁后,删除器会调用 unlock
    对于数据败坏:见下文
    2)异常安全函数的 3 个保证
  3. 基本承诺:抛出异常后,对象仍然处于合法(valid)的状态。但不确定处于哪个状态(对于前面的
    例子,如果发生异常, PrettyMenu 可以继续拥有原背景图像,或是令它拥有某个“缺省”的背景图像,
    但客户无法确定)
  4. 强烈保证:如果抛出了异常, 状态并不会发生发生任何改变。就像没调用这个函数一样
  5. 不抛掷保证:这是最强的保证,函数总是能完成它所承诺的事情(作用于内置类型身上的所有操作
    都提供 nothrow 保证。这是异常安全代码中一个必不可少的关键基础)
    对于前面的 PrettyMenu 对象,可以通过使用智能指针,以及重排 changeBg 的语句顺序来满足“强烈保
    证”:

    class Menu{
    shared_ptr bg;

    };
    void Menu::changeBg(istream& src){
    Lock m1(&mutex); //Lock 以对象管理资源
    bg.reset(new Image(src));
    ++changeCount;
    }

注意,上述实现只能为 PrettyMenu 对象提供“强烈保证”,不能提供完美(即全局状态)的“强烈保证”。
比如 Image 构造函数中移动了 istream& src 的读指针然后再抛出异常,那么系统还是处于一个被改变的
状态。 这是一种对整个系统的副作用,类似的副作用还包括数据库操作,因为没有通用的办法可以撤销
数据库操作。 不过这一点可以忽略,我们暂且认为它提供了完美的强烈保证
copy and swap 策略


“copy and swap”设计策略通常能够为对象提供异常安全的“强烈保证”。当我们要改变一个对象时,先把
它复制一份,然后去修改它的副本,改好了再与原对象交换。 关于 swap 的详细讨论可以参见条款 25。
这种策略用在前面的例子中会像这样:

  1. class Menu{
  2. ...
  3. private:
  4. Mutex mutex;
  5. std::shared_ptr<MenuImpl> pImpl;
  6. };
  7. Menu::changeBg(std::istream& src){
  8. using std::swap; // 见 Item 25
  9. Lock m1(&mutex); // 获得 mutex 的副本数据
  10. std::shared_ptr<MenuImpl> copy(new MenuImpl(*pImpl));
  11. copy->bg.reset(new Image(src)); //修改副本数据
  12. ++copy->changeCount;
  13. swap(pImpl, copy); //置换数据,释放 mutex
  14. }

copy and swap 策略能够为对象提供异常安全的“强烈保证”。但是一般而言,它并不保证整个函数有“强烈
保证”。也就是说,如果某个函数使用 copy and swap 策略为某个对象提供了异常安全的“强烈保证”。但
是这个函数可能调用其它函数,而这些函数可能改变一些全局状态(如数据库状态),那么”整个函数“就
不是”强烈保证“
函数提供的”异常安全保证“通常最高只等于其所调用的各个函数的”异常安全保证“中的最弱者
除此之外, copy and swap 必须为每一个即将被改动的对象作出一个副本,从而可能造成时间和空间上的
问题
3)最终目标是什么
当”强烈保证“不切实际时(比如前面提到的全局状态改变难以保证,或者效率问题),就必须提供”基本
保证“。现实中你或许会发现,可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使它对许多
人而言摇摇欲坠。只要你曾经付出适当的心力试图提供强烈保证,万一实际不可行,使你退而求其次地
只提供基本保证,任何人都不该因此责难你。对许多函数而言, ”异常安全性的基本保证“是一个绝对同情
达理的选择
总的来说就是,应该为自己的函数努力实现尽可能高级别的异常安全,但是由于种种原因并不是说一定
需要实现最高级别的异常安全,而是应该以此为目标而努力。

发表评论

表情:
评论列表 (有 0 条评论,1034人围观)

还没有评论,来说两句吧...

相关阅读