异常安全在某种意义上来说就像怀孕。。。但是稍微想一想。在没有求婚之前我们不能真正的讨论生殖问题。

假设我们有一个表示GUI菜单的类,这个GUI菜单有背景图片。这个类将被使用在多线程环境中,所以需要mutex进行并发控制。

 class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc); // change background
... // image private:
Mutex mutex; // mutex for this object Image *bgImage; // current background image int imageChanges; // # of times image has been changed };

我们看一种PrettyMenu的changeBackground函数的可能实现:

 void PrettyMenu::changeBackground(std::istream& imgSrc)
{ lock(&mutex); // acquire mutex (as in Item 14) delete bgImage; // get rid of old background ++imageChanges; // update image change count
bgImage = new Image(imgSrc); // install new background unlock(&mutex); // release mutex }

1. 异常安全的函数有什么特征

从异常安全的角度来说,这个函数很糟糕。对于异常安全来说有两个要求,上面的实现没有满足任何一个。

当异常被抛出时,异常安全的函数:

  • 不会泄露资源。上面的代码不会通过这个测试,因为如果”new Image(imgSrc)”表达式产生一个异常,unlock就永远不会被调用,当前线程会一直拥有锁。
  • 不允许数据结构被破坏。如果”new Image(imgSrc)”抛出异常,bgImage就会指向一个被销毁的对象。此外,imageChanges却被增加了,但真实的情况是新图片没有被装载。(从另一个方面来说,旧图片被完全清除掉了,所以我猜测你会争辩图片已经被“修改”过了)

处理资源泄露问题很容易,因为Item 13解释过了如何使用对象来管理资源,Item 14引入了Lock类来确保mutex能够被实时的释放掉:

 void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex); // from Item 14: acquire mutex and
// ensure its later release
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}

使用像Lock一样的资源管理类的一个极大的好处是它通常使函数更短小。看一下为什么不再需要对unlock的调用了?作为一个通用的规则,代码越少越好,因为对代码做改动时,出错和理解错误的可能性变低了。

2. 异常安全的三种保证级别

接下来让我们看一看数据结构被损坏的问题。这里我们要做出选择,但是在我们可以进行选择之前,必须对定义这些选择的术语做一下比较。

异常安全的函数提供了如下三种保证的一种:

  • 基本的保证(basic guarantee),这样的函数在抛出异常的时候,程序中的所有东西仍然保持在有效状态。没有对象或者数据被损坏,并且所有对象或者数据保持一个内部一致的状态(例如所有类的约束条件继续被满足)。然而,程序的正确状态可能不能够被预测出来。例如,我们可以实现changeBackground,一旦异常被抛出,PrettyMenu对象可以继续拥有旧的背景图片,但是客户不能够预测拥有的是哪一个。(为了能够找出这个图片,大概需要调用一些成员函数,来告诉它们当前的背景图片是哪一个)
  • 强保证(strong guarantee)。这样的函数在异常被抛出时,程序的状态不会被改变。对这些函数的调用是原子性(atomic)的,意思是如果调用成功了将会完全成功,但如果失败了,程序的状态就像没有被调用一样。

使用提供强保证的函数比只提供基本保证的函数要更加容易,因为调用提供强保证的函数之后,只可能有两种程序状态:函数被正确执行后的状态,或者函数被调用之前的状态。而如果在调用只提供基本保证的函数的时候抛出异常,程序可以进入任何有效状态。

  • 无异常保证(nothrow guarantee)。这样的函数永远不会抛出异常,因为它们总能做到它们所承诺的。所有在内建类型上进行的操作都是无异常的。这是异常安全代码的一个关键的构建基础。

认为带有空异常明细(empty exception specification)的函数是无异常的,这可能看上去是合理的,但事实上不是这样。举个例子,考虑下面的函数:

       int doSomething() throw();   // note empty exception spec.

这并不是说doSomething永远不会抛出异常。它的意思是如果soSomething抛出异常,就会是一个严重的错误,并且会调用意料不到的函数。事实上,doSomething没有提供任何异常安全保证。这个函数的声明(如果有异常明细,也包含异常明细)并没有告诉你这个函数是否是正确的,可移植的或者效率高的,也没有为你提供任何异常安全保证。所有这些特性都由函数的实现来决定,而不是声明。

异常安全的代码必须提供上面三种保证的一种。如果没有提供,它就不是异常安全的。你的选择决定了为你所实现的函数提供哪种保证。除了在处理异常不安全的旧代码时不需要提供异常安全保证之外,异常不安全的代码只有在下面一种情况下才会需要:你的团队做需求分析时发现有对资源泄露和在破环的数据结构上运行程序的需要。

作为普通标准,提供最强异常安全保证是实际的想法。从异常安全的角度来说,不抛出异常的函数才是完美的。但在C++的C部分中很难不去调用可能会抛出异常的函数。使用动态分配内存的任何东西(例如,所有的STL容器)如果发现没有足够的内存可供分配都会抛出一个bad_alloc异常(Item 49)。如果能提供不抛出异常的函数更好,更多的情况是在基本保证强保证之间做出选择。

3. 提供异常安全的两种方法

3.1 使用智能指针

对于changeBackground来说,提供强保证不是多难的事。首先,我们将PrettyMenu的bgImage数据成员的类型从内建的Image*指针替换为一种资源管理智能指针(见 Item 13)。说真的,对于防止资源泄露来说这绝对是一个好方法。它帮我们提供强异常安全保证的事实只是简单对Item 13中的论述(使用对象管理资源是好的设计的基础)做了进一步的加强。在下面的代码中,我将会展示tr1::shared_ptr的使用,因为当进行拷贝时使用tr1::shared_ptr比使用auto_ptr更加直观,因此更受欢迎。

其次,我们对changeBackground中的语句进行重新排序,达到只有image被修改的时候才会增加imageChnages的目的。作为通用准则,一个对象的状态没有被修改就表明一些事情没有发生。

下面是最终的代码:

 class PrettyMenu {
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); // replace bgImage’s internal
// pointer with the result of the
// “new Image” expression
++imageChanges;
}

注意这里不再需要手动delete旧image,因为这由智能指针在内部处理。并且,销毁操作只有在新image成功创建的时候才会发生。更精确的说,只有在参数(new Image(imgSrc)的结果)被成功创建的时候tr1::shared_ptr::reset函数才会被调用。Delete只在reset函数内部被使用,所以如果reset不被调用,delete永远不会被执行。注意资源管理对象的使用再次削减了changeBackground的长度。

正如我所说的,上面的两个修改足以为changeBackground提供强异常安全保证。还有美中不足的就是关于参数imgSrc。如果Image的构造函数抛出异常,输入流的读标记可能会被移动,这个移动致使状态发生变化并且对程序接下来的运行是可见的。如果changeBackground不处理这个问题,它只能提供基本异常安全保证。

3.2 拷贝和交换

把上面的问题放到一边,我们假设changeBackground能够提供强异常安全保证。(你应该能想出一个好的办法来提供强异常安全保证,也许可以将参数类型从istream变为包含image数据的文件的名字。)有一种普通的设计策略也能提供强保证,熟悉它很重要。这个策略叫做“拷贝和交换”(copy and swap。它是很简单的:先对你想要修改的对象做一份拷贝,然后在拷贝上进行所有需要的改动。如果任何修改操作抛出了异常,源对象仍然保持未修改状态。一旦修改完全成功,将源对象和修改后的对象进行不会抛出异常的交换即可(Item 25)。

这往往会把真实的对象数据放入到一个单独的实现对象中,然后提供一个指向这个实现对象的指针。也即是指向实现的指针(pimpl idiom),Item 31中会进行详细描述。PrerttyMenu的实现如下:

 struct PMImpl { // PMImpl = “PrettyMenu
std::tr1::shared_ptr<Image> bgImage; // Impl.”; see below for
int imageChanges; // why it’s a struct
};
class PrettyMenu {
...
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap; // see Item 25
Lock ml(&mutex); // acquire the mutex std::tr1::shared_ptr<PMImpl> // copy obj. data pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
++pNew->imageChanges; swap(pImpl, pNew); // swap the new
// data into place } // release the mutex

在这个例子中,我选择将PMImpl定义为一个结构体而不是类,因为PrettyMenu的封装性通过pImpl的私有性(private)来保证。把PMImpl定义成类会至少和结构体一样好,虽然有一些不方便。如果需要,PMImpl可以被放在PrettyMenu中,但是打包问题(packaging)是我们所关心的。

拷贝和交换策略是处理对象状态问题的卓越方法(状态要么全变要么都不变),但是一般情况下,它不能够保证所有的函数是强异常安全的。想知道为什么,考虑对changeBackground做的一个抽象,someFunc,这个函数使用拷贝和交换策略,但也包含对其它两个函数的调用,f1和f2:

 void someFunc()
{ ... // make copy of local state f1(); f2(); ... // swap modified state into place }

这里如果f1或者f2不是强异常安全的,someFunc就很难是强异常安全的。举个例子,假设f1只提供了基本保证。如果someFunc要提供强保证,必须写代码在调用f1之前确定整个程序的状态,然后捕获f1中的所有异常,如果发生异常则恢复原始状态。

即使f1和f2是强异常安全的,情况也没有任何改观。因为如果f1运行完成后,程序的状态发生了变化,这时候如果f2抛出了异常,程序的状态和调用someFunc之前已经不一样了,即使f2没有修改任何东西。

4. 不能提供强异常安全保证的两种情况

当函数操作只影响本地状态(例如,someFunc只影响调用此函数的对象的状态),提供强异常安全保证是相对容易的。当函数对非本地数据也产生副作用时,提供强保证就相当困难了。例如,如果调用f1的副作用是数据库会被修改,很难让someFunc提供强异常安全。通常来说,对于已经提交的数据库改动,没有方法对其进行回退。其它的数据库客户端可能已经看到了数据库的新状态。

即使你想提供强异常安全保证,上述问题也会阻止你。另外一个问题是效率问题。拷贝和交换的关键点在于首先对对象拷贝进行修改,然后将源数据和修改后的数据进行无异常的交换。这需要对每个要修改的对象都做一份拷贝,这需要时间和空间,你可能不能够或不愿意为其提供这些资源。大家都想获得强异常安全保证,你应该在实际的时候提供它,但不是100%的情况下都是实际可行的。

5. 至少为代码提供基本异常安全保证(遗留代码除外)

如果不切实际,你必须提供基本保证。在实际情况中,你可能发现你可以为一些函数提供强保证,但是在效率和复杂度方面的开销使其变得不再实际。只要你为提供强异常安全保证的函数做出努力了,没有人会因为你提供基本保证而批评你。对许多函数来说,基本保证是最合理的选择。

如果你实现一个函数不提供任何异常安全的保证,事情就不一样了,因为你会一直内疚下去直到证明你是无辜的。所以你应该实现异常安全的代码。但是你可能有所抵触。再考虑一下someFunc的实现,它调用了函数f1和函数f2。假设f2没有提供异常安全保证,连基本保证也没有提供。这就意味着如果在f2内部抛出异常,资源泄露就可能会发生,也可能会出现被破坏的数据结构,例如,有序的数组不再有序,从一个数据结构传递到另一个数据结构的对象被丢失等等。someFunc没有任何方法能够对这些问题做出补偿。如果函数someFunc调用了没有提供异常安全的函数,someFunc自己也不能提供任何保证。

让我们回到怀孕的话题。一个女性要么怀孕了要么没有怀孕。不可能部分的怀孕把。类似的,一个软件系统要么是异常安全的,要么不是。也没有部分异常安全的系统。如果一个系统中有一个没有提供异常安全的函数,那么整个系统也就不是异常安全的,因为对这个函数的调用会导致资源泄露和数据结构的破坏。不幸的是,许多C++遗留代码并没有被实现为异常安全的,所以如今太多的系统都不是异常安全的。

没有任何理由维持这种状态。所以当写新代码或修改现有代码时,对如何使其变得异常安全需要进行仔细的考虑。首先使用对象管理资源(Item 13),这能防止资源泄露。然后为每个函数从三种异常安全保证中选取实际并且最强的那一个,只有在调用遗留代码时让你无可选择的情况下才能勉强接受无安全保证。为函数的使用者和将来的维护人员将你做的决定记录在文档中。一个函数异常安全保证是接口的可见部分,所以在你选择异常安全保证部分时,你应该像选择函数接口的其它方面一样谨慎。

40年前,goto语句被认为是好的实践。现在我们却努力实现结构化控制流(structured control flows)。20年前,全局访问数据被认为是好的实践。现在我们却努力对数据进行封装。10年前,实现出不用考虑异常影响的函数被认为是好的实践。现在我们努力写出异常安全的代码。

与时俱进。活到老,学到老。

6. 总结

    • 异常安全函数不会造成资源泄露,也不允许数据结构被破坏,即使在抛出异常的情况下也如此。这样的函数提供基本的,强的和不抛出异常三种保证。
    • 强保证通常通过拷贝和交换来实现,但为所有函数都提供强保证是不切实际的。
    • 一个函数提供的最强异常安全保证不会强于它所调用函数中提供的最弱异常安全保证。

读书笔记 effective c++ Item 29 为异常安全的代码而努力的更多相关文章

  1. 读书笔记 effective c++ Item 11 在operator=中处理自我赋值

    1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...

  2. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  3. 读书笔记 effective c++ Item 25 实现一个不抛出异常的swap

    1. swap如此重要 Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11).Sw ...

  4. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  5. 读书笔记 effective c++ Item 19 像设计类型(type)一样设计类

    1. 你需要重视类的设计 c++同其他面向对象编程语言一样,定义了一个新的类就相当于定义了一个新的类型(type),因此作为一个c++开发人员,大量时间会被花费在扩张你的类型系统上面.这意味着你不仅仅 ...

  6. 读书笔记 effective c++ Item 8 不要让异常(exceptions)离开析构函数

    1.为什么c++不喜欢析构函数抛出异常 C++并没有禁止析构函数出现异常,但是它肯定不鼓励这么做.这是有原因的,考虑下面的代码: class Widget { public: ... ~Widget( ...

  7. 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

    1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...

  8. 读书笔记 effective c++ Item 17 使用单独语句将new出来的对象放入智能指针

    1. 可能会出现资源泄漏的一种用法 假设我们有一个获取进程优先权的函数,还有一个在动态分类的Widget对象上根据进程优先权进行一些操作的函数: int priority(); void proces ...

  9. 读书笔记 effective c++ Item 26 尽量推迟变量的定义

    1. 定义变量会引发构造和析构开销 每当你定义一种类型的变量时:当控制流到达变量的定义点时,你引入了调用构造函数的开销,当离开变量的作用域之后,你引入了调用析构函数的开销.对未使用到的变量同样会产生开 ...

随机推荐

  1. 纠错输出编码法ECOC

    纠错输出编码法(Error-Correcting Output Codes,ECOC)不仅能够将多类分类问题转化为多个两类问题,而且利用纠错输出码本身具有纠错能力的特性,可以提高监督学习算法的预测精度 ...

  2. Mac下使用Brew搭建PHP(LNMP/LAMP)开发环境

    Mac下搭建lamp开发环境很容易,有xampp和mamp现成的集成环境.但是集成环境对于经常需要自定义一些配置的开发者来说会非常麻烦,而且Mac本身自带apache和php,在brew的帮助下非常容 ...

  3. CSS BUG 总结

    1.IE7 容器使用了滚动条  其子元素中使用 position:relative ,position变成了fixed,从而不随容器的滚动条滚动; 解决: 在其容器元素的属性中也加入 position ...

  4. 用Linux命令行获取本机外网IP地址

    引言:目前获取ip的方法中,ifconfig和ip获取函数得到的都是内网ip.有时候需要获取外网ip,目前通用的做法,是向外部服务器发送请求,解析外部服务器响应,从而得到的自己的外网ip.linux下 ...

  5. ora-04031

    诊断并解决ORA-04031 错误 当我们在共享池中试图分配大片的连续内存失败的时候,Oracle首先清除池中当前没使用的所有对象,使空闲内存块合并.如果仍然没有足够大单个的大块内存满足请求,就会产生 ...

  6. mybatis判断集合为空或者元素个数为零

    mybatis判断集合为空或者元素个数为零: <if test="mlhs != null and mlhs.size() != 0"> and t.mlh_name ...

  7. WebSocket协议再认识

    WebSocket出现之前 在线聊天室.在线客服系统.评论系统.WebIM等这些应用有一个共同点,就是用户不需要去刷新浏览器就能够从服务器获得最新的数据,这就用到了推送技术. WebSocket出现之 ...

  8. SecureCRT 选择Courier New等其他字体.

    http://justwinit.cn/post/5813/ 如何解决SecureCRT无法选择Courier New等其他字体最终解决办法:到C:\Windows\Fonts目录下,找到Courie ...

  9. Nancy简单实战之NancyMusicStore(二):打造首页

    前言 继上一篇搭建好项目之后,我们在这一篇中将把我们NancyMusicStore的首页打造出来. 布局 开始首页之前,我们要先为我们的整个应用添加一个通用的布局页面,WebForm中母版页的概念. ...

  10. 深入浅出ThreadLocal

    前言 ThreadLocal为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰.本文会基于实际场景介绍ThreadLocal如何使用以及内部实现机制. ...