一、可能抛出异常的演示案例
- 下面是一个class,用来表现夹带背景图片的GUI菜单,其中有个互斥器作为并发控制:
void lock(Mutex* pm); //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解锁
class PrettyMenu {
public:
void changeBackground(std::istream& imgSrc) {
lock(&mutex); //取得互斥器
delete bgImage; //删除旧图片
++imageChanges; //修改图像更改次数
bgImage = new Image(imgSrc); //安装新的背景图片
unlock(&mutex); //释放互斥器
}
private:
Mutex mutex; //互斥器
Image* bgImage; //目前使用的背景图片
int imageChanges;//图片被修改的次数
};
- 上面的changeBackground()成员函数不是“异常安全的”。因为异常安全的函数应该有以下两种特性:
- ①不泄露任何资源:上述的代码如果new Image()操作导致异常,那么就永远不会调用unlock,那么互斥器将永远被锁住。因此该成员函数没有保证这一点
- ②不允许数据破坏:如果new Image()操作导致异常,那么bgImage已经被删除了,而且imageChanges数量也被累加了,所以资源被改变了。但是该函数没有保证这一点
二、解决资源泄露的问题
- 这个问题很容易解决,在条款13中讨论了如何以对象管理资源,条款14也介绍了自己设计一个名为Lock的类来管理互斥器,定义如下:
//初始化对象时锁住互斥器,对象释放时自动解除互斥器
class Lock
{
public:
//获得资源
explicit Lock(Mutex* pm) :mutexPtr(pm) {
lock(mutexPtr);
}
//释放资源
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
//重写修改PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);//将互斥器封装在类中进行管理
delete bgImage; //删除旧图片
++imageChanges; //修改图像更改次数
bgImage = new Image(imgSrc); //安装新的背景图片
}
- 上面我们将互斥器对象封装在一个类中,初始化时锁住互斥器,当对象声明周期结束之后不需要我们手动释放互斥器,析构函数会自动帮我们释放互斥器,因此这种方法可以很好的解决资源泄漏的问题
三、解决数据破坏的问题
- 上面介绍了资源泄漏的一般解决办法,现在来关注一下数据破坏的问题。在此之前我们先定义一些术语:
- ①基本承诺:如果异常被抛出,程序内的任何事物都应该保持在有效状态
- ②强烈保证:如果程序抛出异常,程序状态不应该保证。调用这样的函数应该保证:如果函数成功就是完全成功;如果函数执行失败,程序会恢复到“调用函数之前”的状态
- ③不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能
- C++提供两种异常说明:
- 虽然可以使用上面两种异常说明来显式说明函数不会抛出异常,但是如果函数抛出了异常还是允许的
- 异常安全的代码必须提供上述三种保证之一,如果不是这样,那么代码就不是异常安全的
- 对于changeBackground()函数而言,为了保证数据不被破坏,可以更改为下面的代码:
- 使用智能指针来管理Image对象
- 重新排序changeBackground()函数内的语句顺序,使得更换图像之后才累加imageChanges
- 在changeBackground()函数内部不需要手动删除delete旧图像了,因为已经由智能指针自动管理删除了(在reset的内部被调用)
class PrettyMenu {
//...
private:
std::tr1::shared_ptr<Image> bgImage;
};
//重写修改PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
四、copy-and-swap策略
- “三”中虽然解决了数据破坏的问题,但是还有一个问题没有解决,那就是在在changeBackground()函数中,Image的构造函数可能会抛出异常
- copy and swap策略的原则是:为你打算修改的对象(原件)做一份副本,然后在副本身上做修改:
- 如果在副本的身上修改抛出了异常,那么原对象未改变状态
- 如果在副本的身上修改未抛出异常,那么就将修改过的副本与原对象进行置换(swap)
- pimpl idiom手法:
- 将原对象封装到一个类中(或结构)中
- 这种手法称为pimpl idiom,在条款31会讲解
- 对于上面的PrettyMenu来说,典型的写法如下:
演示案例
- 对于上面的PrettyMenu来说,典型的写法如下:
//将bgImage和imageChanges从PrettyMenu独立出来,封装到一个结构中 struct PMImpl { std::tr1::shared_ptr<Image> bgImage; int imageChanges }; class PrettyMenu { //... private: std::tr1::shared_ptr<PMImpl> pImpl; //创建一个该结构 };
- 现在我们重新修改changeBackground()函数:
void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; //见条款25 Lock ml(&mutex); //以pImpl为原件,创建一个副本,然后在副本上做修改 std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc)); pNew->imageChanges++; //如果上面副本的修改没有抛出异常,那么交换副本与原件 swap(pImpl, pNew); }
五、总结
- 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
- 函数提供的“异常安全保证”通常最高值等于其所调用之各个函数的“异常安全保证”中的最弱者