先来看一段代码,假设有一个class用来表现夹带背景图案的GUI菜单
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); //释放互斥器
}
从“异常安全性”来看这个函数很差。“异常安全”有两个条件,当异常被抛出时,带有异常安全性的函数会:
1) 不泄露任何资源
上面的函数没有做到这点,一旦"new Image(imgSrc)"异常了,对unlock的调用就不会执行了,于是互斥器资源被泄露
2) 不允许数据败坏
还是上面的函数,一旦"new Image(imgSrc)"异常了,bgImage就是指向一个已被删除的对象,imageChanges也已被累加,而其实没有新的图像被成功安装起来。
解决方法:
1) 对于资源泄露我们前面讨论过--使用对象来管理资源,如:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex); //利用Lock的对象来管理资源--互斥器
delete bgImage; // 摆脱旧的背景图像
++imageChanges; //修改图像变更次数
bgImage = new Image(imgSrc); //安装新的图像
}
2) 如何保证数据不被破坏 先来看一下异常安全函数的三个级别:
a 基本承诺
如果异常抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
b 强烈保证
如果异常抛出,程序状态不改变。即如类函数成功,就是完全成功,如果函数失败,程序回复到调用函数之前的状态
c 不抛掷(nothrow)保证
承诺绝不抛出异常,因为它们总是能够完成原先承诺的功能。作用于内置类型(如int等)的操作都提供nothrow保证。
异常安全码(Exception-safe code)必须提供上述三种保证之一。否则,它就不是异常安全的。我们的所要做的就是根据我们的代码要求,选择一种级别来实现相应的异常安全的代码。
一般来说,你希望提供nothrow函数,这个是异常安全最好的状态。可是这个往往都办不到。可能的话请得供nothrow保证。
现在我们给出一个针对上面的函数的异常安全的强烈保证的例子:
方案一 首先使用智能指针,之后调整函数内的语句顺序
class PrettyMenu {
...
sdt::trl::shared_ptr<Image> bgImage; //使用智能指针
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); //这里不需要手动delete旧图像,因为智能指针内部处理好了
++imageChange; //更新图像后才累加imageChange
}
方案二 这里还有一个一般化的设计策略,就是copy and swap。原则很简单:为你打算修改的对象做出一个副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
具体的做法,还会使用到pimpl idiom,即把隶属对象的数据从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个实现的对象(implementation object)。如:
struct PMImpl { //原对象的数据成员
std::trl::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu {
private:
Mutex mutex; //互斥器 用于多线程环境
sdt::trl::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
usint std::swap;
Lock ml(&mutex);
sdt::trl::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //获取副本
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
++pNew->imageChagnes;
swap(pImpl, pNew); //交换原对象和副本,释放mutex
}
copy and swap保证了对对象状态做出了全有全无改变的一个很好办法,可是它的效率不是很高。
当你在写新码和修改旧码时,请仔细想想如何让它具备异常安全性。首先是以对象管理资源,那可阻止资源泄露。然后是挑选三个异常安全保证中的某一个实施于你所写的每一个函数身上。
总结:
1) 异常安全函数(Exception safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型,强烈型,不抛异常型
2) “强烈保证”往往能够以copy and swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
3) 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者