假设有个class用来表现夹带背景图案的GUI菜单。这个class希望用于多线程环境,所以它有个互斥体作为并发控制之用:
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc);
...
private:
Mutex mutex;
Image* bgImage;
int imageChanges;
};
下面是PrettyMenu的changeBackground函数的一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc) {
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
从异常安全性来看,这个函数很糟。异常安全有两个条件,而这个函数没有满足其中任何一个条件。
当异常抛出时,异常安全的函数会:
- 不泄露任何资源。 上述代码没有做到这一点,一旦
new Image(imgSrc)
导致异常,对unlock的调用就绝不会指向,于是互斥体永远锁住了。 - 不允许数据破坏。 如果
new Image(imgSrc)
抛出异常,bgImage就指向一个已经被删除的对象,imageChanges也已被累加,而其实没有新的图形被成功安放。
解决资源泄露的问题很容易,条款13讨论过如何以对象管理资源,条款14导入了Lock类作为一种确保互斥体被即使释放的方法:
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
资源泄露问题解决了,现在我们可以专注于解决数据破坏了。
异常安全函数提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此被破坏,所有对象都处于内部前后一致的状态。
- 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认识:如果函数成功,就完全成功,如果函数失败,程序会恢复到调用函数前的状态。
- 不抛异常保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供不抛异常保证。如果真的出现异常,程序会终止运行。
对于changeBackground而言,提供强烈保证几乎不困难。首先将 Image*
的原始指针改为一个资源管理的智能指针,然后重新排列函数内的语句顺序,使得在更换图像之后才累加imageChanges。
class PrettyMenu {
...
std::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
现在,changeBackground提供了强烈的异常安全保证。美中不足的是参数imgSrc。如果Image构造函数抛出异常,有可能输入流的读取记号被一走,changeBackground在解决这个问题之前只提供基本的异常安全保证。
有个一般化的涉及策略很典型地会导致强烈保证,这个策略被称为copy and swap。原则很简单:为你打算修改的对象做一份副本,然后在副本上做出一切必要修改。若有任何修改操作抛出异常,源对象保持未改变状态。所有改变成功后,再将修改过的副本和源对象在一个不抛异常的swap函数完成置换。
实现上通常是将所有隶属对象的数据从原对象放入另一个对象内,然后赋予原对象一个指针,指向那个实现对象。这种手法通常称为pimpl idiom。对PrettyMenu而言,典型写法入下:
struct PMImpl {
std::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu {
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
using std::swap;
Lock ml(&mutex);
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
此例中PMImpl称为一个struct而不是一个class,因为PrettyMenu的数据封装性已经由pImpl是private得到了保证。
copy and swap策略是实现“全有或全无”的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。让我们考虑下面这个例子:
void someFunc {
...
f1();
f2();
...
}
如果f1或f2的异常安全性比强烈保证低,就很难让someFunc称为强烈异常安全。如果f1和f2都是强烈异常安全,情况未必好转。假设f1成功执行,程序状态发生改变,而f2执行失败,恢复到执行前的状态,但此状态和someFunc调用前并不相同。
请记住
- 异常安全函数即使发生异常也不会资源泄露或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- 强烈保证往往能够以copy and swap实现出来,但强烈保证并非对所有函数都有可实现或具备现实意义。
- 函数提供的异常安全保证通常最高只等于其所调用的各个函数的异常安全保证中的最低者。