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

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. 函数提供的 “异常安全保证” 通常最高只等于其所调用之各个函数的 “异常安全保证” 中的最弱者。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容