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.函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

  • 相关阅读:
    python 的基础 学习 第六天 基础数据类型的操作方法 字典
    python 的基础 学习 第五天 基础数据类型的操作方法
    python 的基础 学习 第四天 基础数据类型
    ASP.NET MVC 入门8、ModelState与数据验证
    ASP.NET MVC 入门7、Hellper与数据的提交与绑定
    ASP.NET MVC 入门6、TempData
    ASP.NET MVC 入门5、View与ViewData
    ASP.NET MVC 入门4、Controller与Action
    ASP.NET MVC 入门3、Routing
    ASP.NET MVC 入门2、项目的目录结构与核心的DLL
  • 原文地址:https://www.cnblogs.com/hustcser/p/4217938.html
Copyright © 2011-2022 走看看