zoukankan      html  css  js  c++  java
  • [Effective C++ --029]为“异常安全”而努力是值得的

    假设有个class用来表现夹带背景图案的GUI菜单单,这个class用于多线程环境,所以它有个互斥器(mutex)作为并发控制用:

     1 class PrettyMenu{ 
     2 public: 
     3     ... 
     4     void changeBackground(std::istream& imgSrc); 
     5     ... 
     6 private: 
     7     Mutex mutex; 
     8     Image* bgImage; 
     9     int imageChanges; 
    10 }; 
    11 void PrettyMenu::changeBackground(std::istream& imgSrc) 
    12 { 
    13     lock(&mutex); 
    14     delete bgImage; 
    15     ++imageChanges; 
    16     bgImage = new Image(imgSrc); 
    17     unlock(&mutex); 
    18 }

    从异常安全性的角度看,这个函数很糟。因为没有满足异常安全的两个条件:

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

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

    解决资源泄漏的问题很容易,因为条款13已经教会我们“以对象去管理资源”,而条款14也逃入了Lock class作为一种“确保互斥器被及时释放”的方法:

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

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

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

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

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

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

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

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

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

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

     1 class PrettyMenu{ 
     2     ... 
     3     std::tr1::shared_ptr<Image> bgImage; 
     4     ... 
     5 };
     6 
     7 void PrettyMenu::changeBackground(std::istream& imgSrc) 
     8 { 
     9     Lock ml(&mutex); 
    10     bgImage.reset(new Image(imgSrc)); 
    11     ++imageChanges; 
    12 }

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

    作为策略,桥接模式或者叫做PIMPL的模式可以实现:

    PIMPL模式可以参考我的C++博客。

     1 struct PMImpl{ 
     2     std::tr1::shared_ptr<Image> bgImage; 
     3     int imageChanges; 
     4 }; 
     5 class PrettyMenu{ 
     6     ... 
     7 private: 
     8     Mutex mutex; 
     9     std::tr1::shared_ptr<PMImpl> pImpl; 
    10 }; 
    11 void PrettyMenu::changeBackground(std::istream& imgSrc) 
    12 { 
    13     using std::swap; 
    14     Lock ml(&mutex); 
    15     std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); 
    16     pNew->bgImage.reset(new Image(imgSrc)); //修改副本 
    17     ++pNew->imageChanges; 
    18     swap(pImpl, pNew);                      //置换数据 
    19 }

    在那个副本上做一切必要修改。若有任何修改动作抛出异常,源对象仍然保持未改变状态。待所有改变都成功后,再将修改过的副本和原对象在一个不抛出异常的swap中置换

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

    ◆总结

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

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

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

  • 相关阅读:
    Ubuntu上VNC 配置
    Ubuntu远程桌面xrdp方法
    sudo 免密码
    Ubuntu 12.04 root默认密码? 如何使用root登录?
    DNS 和 IPv6 配置攻略
    计算机专业学习浅谈
    [图像]张正友论文翻译(2)
    [图像]张正友论文翻译(1)
    [图像]用Matlab在图像上画矩形框
    word如何修改尾注
  • 原文地址:https://www.cnblogs.com/hustcser/p/4217938.html
Copyright © 2011-2022 走看看