《Effective C++》学习笔记——条款29

来源:互联网 发布:工业手持数据采集终端 编辑:程序博客网 时间:2024/05/14 07:31

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************





五、Implementations




 

Rule 29:Strive for exception-safe code

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





一、一个例子来引发这个规则


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

class PrettyMenu  {public:  ...  void changeBackground(std::istream& imgSrc);    // 改变背景图像  ...private:  Mutex mutex;    // 互斥器  Image* bgImage;    // 目前的背景图像  int imageChanges;    // 背景图像被改变的次数};

下面是PrettyMenu的changeBackground函数的一个可能实现:

void PrettyMenu::changeBackground(std::istream& imgSrc ){  lock(&mutex);    // 取得互斥器  delete bgImage;    // 摆脱旧的背景图像  ++imageChanges;    // 修改图像变更次数  bgImage = new Image(imgSrc);    // 安装新的背景图像  unlock(&mutex);    // 释放互斥器}


但是这个函数从 "异常安全性" 的观点来看却很糟糕。

——PS:这里插入下 ,

关于异常安全性的 两个条件

① 不泄露任何资源

②不允许数据败坏


本例子对于这两个条件,却是一个也没满足

① 一旦 "new Image(imageSrc)" 导致异常,对unlock的调用就绝对不会执行,于是互斥器就永远被锁住了。

② 如果 "new Image(imgSrc)" 抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。(但从另一个角度来说,旧图像已被消除,所以可能会有异议——图像还是被"改变了" )




二、”异常安全性“的两个条件

1.要解决它关于 资源泄漏的问题 很容易,因为在第13个条款就讨论过如何以 对象管理资源,而条款14也导入了 Lock class 作为一种 “确保互斥器被及时释放”的方法:

void PrettyMenu::changeBackground(std::istream& imgSrc){  Lock m1(&mutex);  delete bgImage;  ++imageChanges;  bgImage = new Image(imgSrc);}


关于“资源管理类”,比如Lock,有个一般性规则—— 较少的代码就是较好的代码,因为出错的机会比较少,而且一旦有所改动,被误解的机会也比较少。


2.资源泄漏 已经搞定了,接下来就是 数据败坏 问题了。

——再次PS:

    关于 异常安全函数(Exception-safe functions),需要提供以下三个保证之一

<1> 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态恐怕不可预料。

<2> 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。

<3> 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

————<3>————

如果我们假设,函数带着“空白的异常明细”者必为nothrow函数,似乎合情合理,其实不尽然。

比如,下面这个例子:

int doSomething()  throw();

这并不代表doSomething绝不会抛出异常,而是说如果doSomething抛出异常,将是严重的错误,会有其他函数被调用,实际上doSomething也许完全没有提供任何异常保证。函数的声明式并不能够告诉你是否它是正确的、可移植的或高效的,也不能够告诉你它是否提供任何异常安全性保证。所有那些性质都由函数实现决定,无关乎声明。

————————


异常安全码必须提供上述的三种保证之一。如果它不这样做,它就不具备异常安全性。因此,我们该为我们所写的每一个函数提供一种保证。

一般而言,你应该会想提供可实施的最强烈保证。从异常安全性的观点看,nothrow函数很棒,但我们很难在 C part of C++ 领域中完全没有调用任何一个可能抛出异常的函数。任何使用动态内存的东西(例如所有STL容器)如果无法找到足够内存以满足要求,通常会抛出一个bad_alloc异常(条款49将详解)。OK,所以如果可能请提供 nothrow 保证,但对大部分函数而言,抉择往往落在基本保证和强烈保证之间。

对changeBackground而言,提供强烈保证几乎不困难。

首先,改变PrettyMenu的bgImage成员变量的类型,从一个类型为Image* 的内置指针改为一个“用于资源管理”的智能指针。

其次,重新排列changeBackground内的语句次序,使得在更换图像之后才累加 imageChanges。

就像这样

class PrettyMenu  {    ...    std::tr1::shared_ptr<Image> bgImage;    ...};void PrettyMenu::changeBackground(std::istream& imgSrc){    Lock m1(&mutex);    // 以"new Image"的执行结果设定bgImage的内部指针    bgImage.reset(new Image(imgSrc));    ++imageChanges;}    

——BUT,注意

这里不再需要手动delete旧图像,因为这个动作已经由智能指针内部处理掉了。

此外,删除动作只发生在新图像被成功创建后,也就是说 tr1::shared_ptr::reset函数 只有在其参数( new Image(imgSrc) 的执行结果 )被成功生成后才会被调用。

delete只在reset函数内被调用,所以如果从未进入那个函数也就绝对不会使用delete。


> 这两个改变几乎足够让 changeBackground 提供强烈的异常安全保证。唯一差的就是 参数imgSrc,因为如果 Image 构造函数跑出异常,有可能输入流的读取记号被移走,这样会给程序的其他部分带来麻烦;因此 在解决这个问题之前, changeBackground 仅仅提供 基本的异常安全保证。




3.对于 copy and swap

在研究这个时候,先假定 changeBackground 提供了 强烈保证。

有个一般化的设计策略会帮助你—— copy and swap

原则很简单,就是为你打算改动的对象 做一个副本,然后在副本上做修改,修改完成后,将副本替换原件。

这样如果修改期间抛出异常,原件是没有收到影响的。


它的实现通常是将所有"隶属对象的数据"从原对象放进另一个对象内,然后赋予原对象一个指针,指向哪个所谓的实现对象( implementation object ),这种手法常常被称作pimpl idiom(在条款31中详述)。

对于本例 PrettyMenu,写法是这样的:

struct PMImp1  {    std::tr1::shared_ptr<Image> bgImage;    int imageChanges;};class PrettyMenu  {    ...private:    Mutex mutx;    std::tr1::shared_ptr<PMImp1> pImp1;};void PrettyMenu::changeBackground(std::istream& imgSrc){    using std::swap;    // 获得mutex的副本数据    Lock m1(&mutex);    std::tr1::shared_ptr<PMImp1> pNew(new PMImp1(*pImp1) );    // 修改副本    pNew->bgImage.reset(new Image(imgSrc));    ++pNew->imageChanges;    // 置换数据,释放mutex    swap(pImp1,pNew);}

有注意到,这里的PMImp1用的是 struct 而不是 class。

因为,PrettyMenu的数据封装性已经由于"pImp1是private" 而获得了保证。

如果令PMImp1为一个class,虽然一样好,但是有时候却麻烦一些。


>1< copy and swap 策略是对对象状态做出 "全有或全无" 改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。

看下面这个例子,就能理解这句话了:

void someFunc(){    ...    // 对local状态做副本    f1();    f2();    ...    // 将修改后的与原对象置换}

这里对副本进行两次函数操作 f1 和 f2。

如果f1 或者 f2 的异常安全性比 ”强烈保证“ 低,就很难让someFunc函数 提供 强烈异常安全。


>2< 效率

copy and swap的关键在于 "修改对象数据的副本,然后在一个不抛异常的函数中将修改后的数据和原件置换",因此必须为每一个即将被改动的对象做出一个副本,这个消耗是有些大的。

所以,这个 强烈保证 并非在 任何时刻 都显得实际。





4.强烈保证    基本保证

当强烈保证不切实际时,我们要退而求其次寻求基本保证,对许多函数而言, 异常安全性之基本保证  是一个绝对通情达理的选择。

一个软件系统要不就具备异常安全性,要不然就全然否定,没有所谓的”局部异常安全系统“。如果系统内有一个(唯有一个)函数不具备异常安全性,整个系统就不具备异常安全性,因为调用那个(不具备异常安全性)函数有可能导致资源泄露或数据结构败坏。不幸的是许多老旧C++代码并不具备异常安全性,所以今天许多系统仍然不能够说是”异常安全”的。

当我们撰写新代码或者修改旧代码时,请仔细想想如何让它具备异常安全性。

>首先是“以对象管理资源”,这样可以阻止资源泄露。

>然后是挑选三个“异常安全保证”中的某一个实施于你缩写的每一个函数身上。应该挑选“现实可执行”条件下的最强烈等级,只有当你的函数调用了传统代码,才别无选择的让它“无任何保证”。

还要将你的决定写成文档,这样一来为你的函数用户着想,二来为将来的维护者着想。

>函数的异常安全性保证 是其可见接口的一部分,所以应该慎重选择,就像选择函数接口的其他任何部分一样。




——作者的感叹

四十年前,满载goto的代码被视为一种美好实践,而今我们却致力写出结构化控制流;

二十年前,全局数据被视为一种美好的实践,而今我们却致力于数据的封装;

十年前,撰写“未将异常考虑在内”的函数被视为一种美好实践,而今我们致力于写出“异常安全码“。

时间不断前进,我们与时俱进。





☆ 请注意 ☆

▪ 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

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

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






***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

0 0