条款 29 :为“异常安全”而努力是值得的
Strive for exception-safe code.
假设有一个class用来表现带背景的GUI菜单。希望其用于多线程环境,所以他得有一个互斥器作为并发控制之用。
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc);//改变背景图片
...
private:
Mutex mutex;//互斥器
Image* bgImage;//目前得背景图像
int imageChanges;//背景图像被改变得次数
}
下面是changeBackground函数得可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc){
lock(&mutex);//取得互斥器
delete bgImage;//摆脱旧的背景图像
++imageChanges;//修改图像的更改次数
bgImage = new Image(imgSrc);//放置新的图像
unlock(&mutex);//释放互斥器
}
从“异常安全性”的角度来看,这个代码糟糕至极。
“异常安全”有两个条件,但是这个函数没有满足任意一个。这两个条件是,当异常被抛出时,有“异常安全”的函数就会:
- **不泄露任何资源。**上面的代码没有做到这一点,因为一旦new出现问题,对于unock()的调用就不会执行到,于是互斥器永远被锁住。
- **不允许数据败坏。**如果new出现异常,bgImage就指向了一个已经被删除的对象,imageChanges也已经被累加,但是实际上却没有改变。
解决资源泄漏的问题很容易,回想条款13和条款14.这里我们再说说Lock class:
Lock class用来管理互斥锁,的基本结构由RAII守则支配。也就是资源再构造期间获得,在析构期间释放:
class Lock{
public:
explicit Lock(Muutex* pm):mutexPtr(pm){//获得资源
lock(mutexPtr);
}
~Lock(){unlock(mutexPtr);}//释放资源
private:
Mutex* mutexPtr;
}
使用Lock class 我们也就遵循了RAII守则,同时也解决上述糟糕代码的问题:
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock ml(&mutex);//获得互斥器并确保其稍后被释放
delete bgImage;//摆脱旧的背景图像
++imageChanges;//修改图像的更改次数
bgImage = new Image(imgSrc);//放置新的图像
}
这里再次实践了以对象管理资源的好处。这里解决了资源泄漏的问题,现在我们可以专注数据败坏的问题了。在此之前我们先来看看一些基本术语:
**异常安全函数(Exception-safe functions)**提供以下三个保证之一:
- **基本承诺:**如果异常被抛出,程序内的任何事物仍然保持在有效状态下。所有对象内部处于一种前后一致的状态,然而程序的现实状态未可知。举个例子,例如上述new失败后,PrettyMenu类应该具有原背景图或被置为缺省的图,具体是那张图客户没有控制权。
- **强烈保证:**如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功则完全成功,否则程序会恢复到调用之前的状态。这比基本承诺更加简单直接,因为他保证程序只有两个状态,要么成功,要么不成功;而基本承诺在调用之后程序会处于任何状态(只要该状态合法)。
- **不抛弃保证:**承诺绝不抛出异常,因为他总能完成他们原先承诺的功能。作用于内置类型身上的所有操作都应该有nothrow保证。这是异常安全代码中一个必不可少的关键基础材料。
异常安全代码必须要提供上述保证之一。
一般你会想直接提供notrhrow保证,但是实际情况是一个函数很难不抛出异常,所以一般我们都在基本承诺和强烈保证之间选择。
这里让我们再实践以对象管理资源的忠告,来防止数据败坏,看下列代码:
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旧图像了,因为这个动作被智能指针自动处理了。而且删除动作只有new成功之后才会发生,准确的说shared_ptr::reset函数只有其参数(也就是new Image的执行结果)被成功生成之后才会被调用。delete只在reset内被调用,如果未曾进入过该函数,那么也就不会调用delete了。
然而上述解决方法还只是提供了基本保证,因为如果Image构造函数如果出现异常,可能输入流的读取记号已经被移走。
下面给出提供强烈保证的方法:
我们首先了解一下Copy and Swap:为你打算修改的对象(原件)做出一个副本(copy),然后在那个副本中做一切必要的修改。若任何修改抛出异常,你的原件保持不变。待所有修改都成功之后,再将修改之后的副本的原对象在一个不抛出异常的操作中置换(swap)。
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);//获得互斥器并确保其稍后被释放
//获得mutex的副本数据
std::tr1::shared_ptr<PMImpl> PNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));//修改副本
++pNew->imageChanges;
swap(pImpl,pNew);//安全的交换
}
不存在在局部异常安全系统,作者给系统安全性打了一个比方:一个女生怀孕了就是怀孕了,没有就是没有,不能说部分怀孕。同样也不存在系统局部是安全的这么一说。
请记住
异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型,强烈型,不抛出异常型。
“强烈保证 ”往往能够以copy and swap实现出来,但是强烈保证并不是对所有函数都有可实现或具备实现意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。