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

        异常安全性(Exception safety)有几分像是......呃......怀孕。但等等,在我们完成求偶之前,实在无法确定地谈论生育。

        假设有个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);                 // 释放互斥器
}

        从“异常安全性”的观点来看,这个函数很糟。“异常安全”有两个条件,而这个函数没有满足其中任何一个条件。当异常被抛出时,带有异常安全性的函数会:

  • 不泄露任何资源。上述代码没有做到这一点,因为一旦“new Image(imgSrc)”导致异常,对unlock的调用绝不会执行,于是互斥器就永远被把持住了。
  • 不允许数据败坏。如果“new Image(imgSrc)”抛出异常,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实并没有新的图像被成功安装起来。(但从另一个角度说,旧图像已被消除,所以你可能会争辩说图像还是被“改变了”)。

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

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

        把资源泄漏抛诸脑后,现在我们可以专注解决数据的败坏了。此刻我们需要做个抉择,但是在我们能够抉择之前,必须先面对一些用来定义选项的术语。

        异常安全函数(Exception-safe functions)提供以下三个保证之一:

  • 基本承诺:如果异常抛出,程序内任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。然而程序的现实状态恐怕不可预料。例如,我们可以撰写changeBackground使得一旦有一次被抛出时,PrettyMenu可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。
  • 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就完全成功,如果函数失败,程序会回到“调用函数之前”的状态。
  • 不抛异常(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

        对changeBackground而言,提供强烈保证几乎不困难。首先改变PrettyMenu的bgImage成员变量的类型,从一个类型为Image*的内置指针改为一个“用于资源管理”的智能指针(见条款13)。坦白说,这个好构想纯粹只是帮助我们防止资源泄漏。

        第二,我们重新排列changeBackground内的语句次序,使得在更换图像之后才累加imageChanges。一般而言这是个好策略:不要为了表示某件事情而改变对象状态,除非那件事情真的发生了。

        下面是结果:

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

        注意,这里不再需要手动delete旧图像,因为这个动作已经有智能指针内部处理掉了。此外,删除动作只发生在新图像被成功创建之后。更正确地说,tr1::shared_ptr::reset函数只有在其参数(也就是“new Image(imgSrc)”的执行结果)被成功生成之后才会被调用。delete只在reset函数内被使用,所以如果从未进入那个函数也就绝对不会使用delete。也请注意,以对象(tr1::shared_ptr)管理资源(这里是动态分配而得的Image)再次缩减了changeBackground的长度。

        这两个改变几乎足够让changeBackground提供强烈的异常安全保证。美中不足的是参数imgSrc。如果Image构造函数抛出异常,有可能输入流(input stream)的读取记号(read marker)已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以changeBackground在解决这个问题之前只提供基本的异常安全保证。

        有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,源对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。

        实现上,通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种手法常被称为pimpl idiom,条款31详细描述了它。对PrettyMenu而言,典型写法如下:

struct PMImpl {                             // PMImpl="PrettyMenu Impl";
	std::tr1::shared_ptr<Image> bgImage;  // 稍后说明为什么它是个struct 
	int imageChanges;
}
class PrettyMenu {
	...
private:
	Mutex mutex;
	std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	using std::swap;                                        // 见条款25
	Lock ml(&mutex);                                        // 获得mutex的副本数据
	std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
	pNew->bgImage.reset(new Image(imgSrc));                 // 修改副本
	++pNew->imageChanges;
	swap(pImpl, pNew);                                      // 置换(swap)数据,释放mutex
}

        此例之中我选择让PMImpl称为一个struct而不是一个class,这是因为PrettyMenu的数据封装性已经由于“pImpl是private”而获得了保证。如果令PMImpl为一个class,虽然一样好,有时候却不太方便(但也保持了面向对象纯度)。比如打包问题。

       “copy-and-swap”策略是对对象状态做出“全有或全无”改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。比如函数中包含其它函数:

void someFunc()
{
	...      // 对local状态做一份副本
	f1();
	f2();
	...      // 将修改后的状态置换过来
}

        上述这种情况就很难保证someFunc的异常安全性了,即使f1和f2都是“强烈异常安全”,情况并不好转。毕竟如果f1圆满结束,程序状态在任何方面都可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同,甚至当f2没有改变任何东西也是如此。

        问题出在“连带影响”(side effects)。如果函数只操作局部性状态(local state,例如someFunc只影响其“调用者对象”的状态),便相对容易地提供强烈保证。但是当函数对“非局部性数据”(non-local data)有连带影响时,提供强烈保证就困难得多。举个例子,如果调用f1带来的影响是某个数据库被改动了,那就很难让someFunc具备强烈安全性。一般而言在“数据库修改动作”送出之后,没有什么做法可以取消并恢复数据库旧观,意味数据库的其他客户可能已经看到了这一笔新数据。

请记住

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

个人观点:在实际项目开发中,我们应该避免内存泄漏和数据被破坏的情况,但是要做到上述所说的不抛异常型函数很难,会耗费大量的时间去设计,增加项目开发成本。不过作为开发者,至少要了解这方面可能存在的问题,争取写出优质的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值