Effective C++条款29:实现之(为“异常安全”而努力是值得的)
原创
©著作权归作者所有:来自51CTO博客作者董哥的黑板报的原创作品,请联系作者获取转载授权,否则将追究法律责任
一、可能抛出异常的演示案例
- 下面是一个class,用来表现夹带背景图片的GUI菜单单,其中有个互斥器作为并发控制:
void lock(Mutex* 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;
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
- 上面我们将互斥器对象封装在一个类中,初始化时锁住互斥器,当对象声明周期结束之后不需要我们手动释放互斥器,析构函数会自动帮我们释放互斥器,因此这种方法可以很好的解决资源泄漏的问题
三、解决数据破坏的问题
- 上面介绍了资源泄漏的一般解决办法,现在来关注一下数据破坏的问题。在此之前我们先定义一些术语:
- ①基本承诺:如果异常被抛出,程序内的任何事物都应该保持在有效状态
- ②强烈保证:如果程序抛出异常,程序状态不应该保证。调用这样的函数应该保证:如果函数成功就是完全成功;如果函数执行失败,程序会恢复到“调用函数之前”的状态
- ③不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能
- 虽然可以使用上面两种异常说明来显式说明函数不会抛出异常,但是如果函数抛出了异常还是允许的
- 异常安全的代码必须提供上述三种保证之一,如果不是这样,那么代码就不是异常安全的
- 对于changeBackground()函数而言,为了保证数据不被破坏,可以更改为下面的代码:
- 使用智能指针来管理Image对象
- 重新排序changeBackground()函数内的语句顺序,使得更换图像之后才累加imageChanges
- 在changeBackground()函数内部不需要手动删除delete旧图像了,因为已经由智能指针自动管理删除了(在reset的内部被调用)
class PrettyMenu {
private:
std::tr1::shared_ptr<Image> bgImage;
};
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,在条款31会讲解
- 对于上面的PrettyMenu来说,典型的写法如下:
演示案例
- 对于上面的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;
Lock ml(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
pNew->imageChanges++;
swap(pImpl, pNew);
}
五、总结
- 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
- 函数提供的“异常安全保证”通常最高值等于其所调用之各个函数的“异常安全保证”中的最弱者