您的位置:首页 > 其它

努力做到异常安全

2015-07-21 14:17 253 查看
http://blog.sina.com.cn/s/blog_6002b97001015vr8.html

下面这个class希望用于多线程环境,所以它有个互斥量作为并发控制:

class PrettyMenu

{

public:

...

voidchangeBackground(istream&imgSrc); //改变背景图像

...

private:

Mutexmutex; //互斥器

Image*bgImage; //目前的背景图像

intimageChanges; //背景图像被改变的次数

};

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

void PrettyMenu::changeBackground(istream&imgSrc)

{

lock(&mutex);

deletebgImage;

++imageChanges;

bgImage = newImage(imgSrc);

unlock(&mutex);

}

从“异常安全”的角度看,这个函数很不好。“异常安全”有两个条件,而这个函数没有满足一个。

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

(1)不泄露任何资源。但是上面的代码,一旦new出现异常,互斥器就永远把持住了。

(2)不允许数据败坏。一旦new出现异常,bgImage就指向一个被删除的对象,imageChanges也被累加。

解决资源泄漏的问题很容易,用对象管理资源即可,如:

void PrettyMenu::changeBackground(istream&imgSrc)

{

Lockm1(&mutex); //资源管理类,现在不需要调用unlock了

delete bgImage;

++imageChanges;

bgImage = new Image(imgSrc);

}

现在让我们关注数据败坏。异常安全函数必须提供以下三个保证之一:

(1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,即所有对象处于一种内部前后一致的状态。然而程序的现实状态不可预料。如:我们可以使得changeBackground一旦有异常被抛出,PrettyMenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况(除非调用某个成员函数)。如果调用一个只提供基本承诺的函数,而真的出现异常,程序有可能处于任何状态——只要那个状态合法。

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

(3)不抛掷(nothrow)保证。承诺不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供该保证。

如果我们假设,函数带有“空白的异常明细”者为nothrow函数,是错误的:

int doSomething()throw(); //空白的异常明细

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

异常安全必须提供上述三个保证之一。如果他不这样做,它就不具备异常安全性。

一般而言你应该会想提供可实施之最强保证。但是nothrow在实际中往往做不到,所以对大部分函数而言,抉择往往落在基本保证和强烈保证之间。

对changeBackground而言,提供强烈保证几乎不困难。下面是结果:

class PrettyMenu

{

...

tr1::shared_ptr<Image> bgImage;

...

};

void PrettyMenu::changeBackground(istream&imgSrc)

{

Lockm1(&mutex);

bgImage.reset(newImage(imgSrc));

++imageChanges;

}

注意:这里不需要手动delete旧图像,因为这个动作已经由智能指针内部处理掉了。此外,删除动作只发生在新图像被成功创建之后。更正确的说,tr1::shared_ptr::reset函数只有在其参数被成功生成之后才会被调用。delete只在reset函数内被使用,所以如果从未进入那个函数也就绝对不会使用delete。但美中不足的是参数imgSrc。如果Image构造函数抛出异常,有可能输入流的读取记号已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以changeBackground在解决这个问题之前只提供基本的异常安全保证。

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

实现上通常是将所有“录属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(即副本)。这种手法被称为pimplidiom。对PrettyMenu而言,典型写法如下:

struct PMImpl

{

tr1::shared_ptr<Image> bgImage;

int imageChanges;

};

class PrettyMenu

{

...

private:

Mutexmutex;

tr1::shared_ptr<PMImpl> pImpl;

};

void PrettyMenu::changeBackground(istream&imgSrc)

{

usingstd::swap; //很重要

Lockm1(&mutex);

tr1::shared_ptr<PMImpl> pNew(newPMImpl(*pImpl));

pNew->bgImage.reset(new Image(imgSrc));

++pNew->imageChanges;

swap(pImpl,pNew);

}

让PMImpl称为一个struct而不是一个class,是因为PrettyMenu的数据封装性已经由“PImpl是private”而获得了保证。

“copy andswap”策略是对对象状态做出“全有或全无”改变的一个很好的方法,但一般而言它并不保证整个函数都有强烈的异常安全性(如果在整个函数中还调用其他的函数,而那些函数中只要有一个不是强烈安全的,整个函数就不是强烈安全的,即使另外那些函数也全是强烈安全的,整个函数也不一定是强烈安全的,因为程序的状态可能在中间的某个地方已经改变,如“数据库修改动作”被送出之后,就很难恢复了)。

“copy andswap”策略还有效率的问题,因为需要创建副本。所以强烈保证不一定时时刻刻都是实际的,如果不行,你应该提供“基本保证”,况且对许多函数而言,”基本保证“绝对是一个通情达理的选择。

注意:(1)如果调用的函数没有提供任何异常安全性保证,则该函数本身也不可能提供任何保证。(2)

如果系统内有一个函数不具备异常安全性,整个系统就不具备异常安全性。

让你自己的代码具备异常安全性:(1)以对象管理资源;(2)挑选三个”异常安全性“中的某一个实施于你所写的每一个函数身上。你应该挑选”显示可操作“条件的最强烈等级。只有当你的函数调用了传统代码,才别无选择地将他设为”无任何保证“。

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

总结:(1)异常安全函数即使发生异常也不会泄露资源或允许任何数据败坏。这样的函数区分为三种可能的保证:基本型、强烈性、不抛出异常型;(2)”强烈保证“往往能够以copyandswap实现出来,但”强烈保证“并非所有函数都和实现或具备现实意义;(3)函数提供的”异常安全保证“通常最高只等于其所调用之各个函数的”异常安全性“中的最弱者。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: