美文网首页C++ 杂记
条款 29:为 “异常安全” 而努力是值得的

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

作者: 赵者也 | 来源:发表于2017-07-29 21:09 被阅读2次

    Effective C++ 中文版 第三版》读书笔记

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

    有个 class 用来表现夹带背景图案的 GUI 菜单单,这个 class 用于多线程环境:

    class PrettyMenu { 
    public: 
        ... 
        void changeBackground(std::istream& imgSrc); 
        ... 
    
    private: 
        Mutex mutex; 
        Image* bgImage; 
        int imageChanges; 
    }; 
    
    void PrettyMenu::changeBackground(std::istream& imgSrc) 
    { 
        lock(&mutex); 
        delete bgImage; 
        ++imageChanges; 
        bgImage = new Image(imgSrc); 
        unlock(&mutex); 
    }
    

    从异常安全性的角度看,这个函数很糟。“异常安全” 有两个条件:当异常被抛出时,带有异常安全性的函数会:

    不泄露任何资源。上述代码没有做到这一点,因为一旦 “new Image(imgSrc)” 导致异常,对 unlock 就不会执行,于是互斥器就永远被把持住了。

    不允许数据破坏。如果 “new Image(imgSrc)” 抛出异常,bgImage 就指向一个已被删除的对象,imageChanges 也已被累加,而其实并没有新的图像被 成功安装起来。

    解决资源泄漏的问题很容易,

    void PrettyMenu::changeBackground(std::istream& imgSrc) 
    { 
        Lock ml(&mutex);//来自条款14; 
        delete bgImage; 
        ++imageChanges; 
        bgImage = new Image(imgSrc); 
    }
    

    关于“资源管理类”如 Lock,一个最棒的事情是,它们通常使函数更短。较少的代码就是较好的代码,因为出错的机会比较少。

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

    基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的 class 约束条件都继续获得满足)。然而程序的现实状态恐怕不可预料。如上例 changeBackground 使得一旦有异常被抛出时,PrettyMenu 对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,它们恐怕必须调用某个成员函数以得知当时的背景图像是什么。

    强烈保证:如果异常被抛出, 程序状态不改变。如果函数成功,就是完全成功,否则,程序会回复到“调用函数之前”的状态。

    不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(如 ints,指针等等)上的所有操作都提供 nothrow 保证。带着“空白异常明细”的函数必为 nothrow 函数,其实不尽然

    int doSomething() throw(); // “空白异常明细”
    

    这并不是说 doSomething 绝不会抛出异常,而是说如果抛出异常,将是严重错误,会有你意想不到的函数被调用。实际上 doSomething 也许完全没有提供任何异常保证。函数的声明式(包括异常明细)并不能告诉你是否它是正确的、可移植的或高效的,也不能告诉你它是否提供任何异常安全性保证。

    异常安全码(Exception-safe code)必须提供上述三种保证之一。否则,它就不具备异常安全性。

    一般而言,应该会想提供可实施的最强烈保证。nothrow 函数很棒,但我们很难再 C part of C++ 领域中完全没有调用任何一个可能抛出异常的函数。所以大部分函数而言,抉择往往落在基本保证和强烈保证之间

    对 changeBackground 而言,首先,从一个类型为 Image* 的内置指针改为一个 “用于资源管理” 的智能指针,第二,重新排列 changeBackground 内的语句次序,使得在更换图像之后再累加 imageChanges。

    class PrettyMenu{ 
        ... 
        std::tr1::shared_ptr<Image> bgImage; 
        ... 
    };
    
    void PrettyMenu::changeBackground(std::istream& imgSrc) 
    { 
        Lock ml(&mutex); 
        bgImage.reset(new Image(imgSrc)); 
        ++imageChanges; 
    }
    

    不再需要手动 delete 旧图像,只有在 reset 在其参数(也就是 “new Image(imgSrc)” 的执行结果)被成功生成之后才会被调用。美中不足的是参数 imgSrc。如果 Image 构造函数抛出异常,有可能输入流的读取记号(read marker)已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以在解决这个之前只提供基本点异常安全保证。

    有一个一般化的策略很典型会导致强烈保证,被称为 “copy and swap”:为打算修改的对象做一个副本,在那个副本上做一切必要修改。若有任何修改动作抛出异常,源对象仍然保持未改变状态。待所有改变都成功后,再将修改过的副本和原对象在一个不抛出异常的 swap 中置换。

    实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予源对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。对 PrettyMenu 而言,典型的写法如下:

    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); 
        std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); 
        pNew->bgImage.reset(new Image(imgSrc)); // 修改副本 
        ++pNew->imageChanges; 
        swap(pImpl, pNew);// 置换数据 
    }
    

    copy and swap 策略虽然做出“全有或全无”改变的一个好办法,但一般而言并不保证整个函数有强烈的异常安全性。

    如 someFunc。使用 copy-and-swap 策略,但函数还包括对另外连个函数 f1 和 f2 的调用:

    void someFunc()
    {
        …
        f1();
        f2();
        …
    }
    

    显然,如果 f1 或 f2 的异常安全性比 “强烈保证” 低,就很难让 someFunc 成为 “强烈异常安全”。如果 f1 和 f2 都是 “强烈异常安全”,情况并不因此好转。毕竟,如果 f1 圆满结束,程序状态在任何方面都有可能有所改变,因此如果 f2 随后抛出异常,程序状态和 someFunc 被调用前并不相同,甚至当 f2 没有改变任何东西时也是如此。

    问题出现在 “连带影响”,如果由函数只操作局部状态,便相对容易的提供强烈保证,但是函数对 “非局部性数据” 有连带影响时,提供强烈保证就困难的多。例如,如果调用 f1 带来的影响是某个数据库被改动了,那就很难让 someFunc 具备强烈安全性。另一个主题是效率。copy-and-swap 得好用你可能无法(或不愿意)供应的时间和空间。所以,“强烈保证” 并不是在任何时候都显得实际。

    当 “强烈保证” 不切实际时,你就必须提供 “基本保证”。

    你应该挑选 “现实可操作” 条件下最强烈等级,只有当你的函数调用了传统代码,才别无选择的将它设为 “无任何保证”。

    请记住:

    1. 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
    2. “强烈保证” 往往能够以 copy-and-swap 实现出来,但 “强烈保证” 并非对所有函数都可实现或具备现实意义。
    3. 函数提供的 “异常安全保证” 通常最高只等于其所调用之各个函数的 “异常安全保证” 中的最弱者。

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/koqzkxtx.html