分享
 
 
 

翻译:Effective C++, 3rd Edition, Item 29: 争取异常安全(exception-safe)的代码(上)

王朝c/c++·作者佚名  2006-01-31
窄屏简体版  字體: |||超大  

Item 29: 争取异常安全(exception-safe)的代码

异常安全(Exception safety)意义深远(pregnancy)……但是,请把这个想法保持一会儿。我们不能真正地议论重生(reproduction),直到我们排除困难去争取(courtship)它。(此段作者使用的 3 个词均有双关含义,pregnancy 也可直译为怀孕,妊娠,reproduction 也可直译为繁殖,courtship 也可直译为求爱。——译者注)

假设我们有一个类,代表带有背景图像的 GUI 菜单。这个类被设计成在多线程环境中使用,所以它有一个用于并行控制(concurrency control)的互斥体(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

}

从异常安全的观点看,这个函数烂到了极点。异常安全有两条要求,而这里全都没有满足。

当一个异常被抛出,异常安全的函数应该:

没有资源泄露。上面的代码没有通过这个测试,因为如果 "new Image(imgSrc)" 表达式产生一个异常,对 unlock 的调用就永远不会执行,而那个互斥体也将被永远挂起。不允许数据结构恶化。如果 "new Image(imgSrc)" 抛出异常,bgImage 被遗留下来指向一个被删除对象。另外,尽管并没有将一张新的图像设置到位,imageChanges 也已经被增加。(在另一方面,旧的图像被明确地删除,所以我料想你会争辩说图像已经被“改变”了。)规避资源泄露问题比较容易,因为 Item 13 解释了如何使用对象管理资源,而 Item 14 又引进了 Lock 类作为一种时尚的确保互斥体被释放的方法:

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 的调用不再需要了吗?作为一个一般的规则,更少的代码就是更好的代码。因为在改变的时候这样可以较少误入歧途并较少产生误解。

随着资源泄露被我们甩在身后,我们可以把我们的注意力集中到数据结构恶化。在这里我们有一个选择,但是在我们能选择之前,我们必须先面对定义我们的选择的术语。

异常安全函数提供下述三种保证之一:

函数提供基本保证(the basic guarantee),允诺如果一个异常被抛出,程序中剩下的每一件东西都处于合法状态。没有对象或数据结构被破坏,而且所有的对象都处于内部调和状态(所有的类不变量都被满足)。然而,程序的精确状态可能是不可预期的。例如,我们可以重写 changeBackground,以致于如果一个异常被抛出,PrettyMenu 对象可以继续保留原来的背景图像,或者它可以持有某些缺省的背景图像,但是客户无法预知到底是哪一个。(为了查明这一点,他们大概必须调用某个可以告诉他们当前背景图像是什么的成员函数。)函数提供强力保证(the strong guarantee),允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数在感觉上是极其微弱的,如果它们成功了,它们就完全成功,如果它们失败了,程序的状态就像它们从没有被调用过一样。与提供强力保证的函数一起工作比与只提供基本保证的函数一起工作更加容易,因为调用提供强力保证的函数之后,仅有两种可能的程序状态:像预期一样成功执行了函数,或者继续保持函数被调用时当时的状态。与之相比,如果调用只提供基本保证的函数引发了异常,程序可能存在于任何合法的状态。

函数提供不抛出保证(the nothrow guarantee),允诺决不抛出异常,因为它们只做它们答应要做的。所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。假定一个带有空的异常规格(exception specification)的函数是不抛出的似乎是合理的,但这不一定正确的。例如,考虑这个函数:

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

这并不是说 doSomething 永远不会抛出异常;而是说如果 doSomething 抛出一个异常,它就是一个严重的错误,应该调用 unexpected 函数 [1]。实际上,doSomething 可能根本不提供任何异常保证。一个函数的声明(如果有的话,也包括它的异常规格(exception specification))不能告诉你一个函数是否正确,是否可移植,或是否高效,而且,即便有,它也不能告诉你它会提供哪一种异常安全保证。所有这些特性都由函数的实现决定,而不是它的声明能决定的。

[1] 关于 unexpected 函数的资料,可以求助于你中意的搜索引擎或包罗万象的 C++ 课本。(你或许有幸搜到 set_unexpected,这个函数用于指定 unexpected 函数。)

异常安全函数必须提供上述三种保证中的一种。如果它没有提供,它就不是异常安全的。于是,选择就在于决定你写的每一个函数究竟要提供哪种保证。除非要处理遗留下来的非异常安全的代码(本 Item 稍后我们要讨论这个问题),只有当你的最高明的需求分析团队为你的应用程序识别出的一项需求就是泄漏资源以及运行于被破坏的数据结构之上时,不提供异常安全保证才能成为一个选项。

作为一个一般性的规则,你应该提供实际可达到的最强力的保证。从异常安全的观点看,不抛出的函数(nothrow functions)是极好的,但是在 C++ 的 C 部分之外部不调用可能抛出异常的函数简直就是寸步难行。使用动态分配内存的任何东西(例如,所有的 STL 容器)如果不能找到足够的内存来满足一个请求(参见 Item 49),在典型情况下,它就会抛出一个 bad_alloc 异常。只要你能做到就提供不抛出保证,但是对于大多数函数,选择是在基本的保证和强力的保证之间的。

在 changeBackground 的情况下,提供差不多的强力保证并不困难。首先,我们将 PrettyMenu 的 bgImage 数据成员的类型从一个内建的 Image* 指针改变为 Item 13 中描述的智能资源管理指针中的一种。坦白地讲,在预防资源泄漏的基本原则上,这完全是一个好主意。它帮助我们提供强大的异常安全保证的事实进一步加强了 Item 13 的论点——使用对象(诸如智能指针)管理资源是良好设计的基础。在下面的代码中,我展示了 tr1::shared_ptr 的使用,因为当进行通常的拷贝时它的更符合直觉的行为使得它比 auto_ptr 更可取。

第二,我们重新排列 changeBackground 中的语句,以致于直到图像发生变化,才增加 imageChanges。这是一个很好的策略——直到某件事情真正发生了,再改变一个对象的状态来表示某事已经发生。

这就是修改之后的代码:

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;

}

注意这里不再需要手动删除旧的图像,因为在智能指针内部已经被处理了。此外,只有当新的图像被成功创建了删除行为才会发生。更准确地说,只有当 tr1::shared_ptr::reset 函数的参数("new Image(imgSrc)" 的结果)被成功创建了,这个函数才会被调用。只有在对 reset 的调用的内部才会使用 delete,所以如果这个函数从来不曾进入,delete 就从来不曾使用。同样请注意一个管理资源(动态分配的 Image)的对象(tr1::shared_ptr)的使用再次缩短了 changeBackground 的长度。

正如我所说的,这两处改动差不多有能力使 changeBackground 提供强力异常安全保证。美中不足的是什么呢?参数 imgSrc。如果 Image 的构造函数抛出一个异常,输入流(input stream)的读标记(read marker)可能已经被移动,而这样的移动就成为对程序的其它部分来说可见的一个状态的变化。直到 changeBackground 着手解决这个问题之前,它只能提供基本异常安全保证。

(未完待续)

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有