假设我们有一个类来表示带有背景图像的GUI菜单。这个类被设计为支持多线程环境,所以它有一个互斥量来控制并发:
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc); // 改变背景图像
...
private:
Mutex mutex; // 这个对象的互斥量
Image* bgImage; // 当前背景图像
int imageChanges; // 图像被改变的次数
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); // 取得互斥量(见条款14)
delete bgImage; // 去除旧的背景
++imageChanges; // 更新图像变化计数
bgImage = new Image(imgSrc); // 安装新的背景
unlock(&mutex); // 释放互斥量
}
当异常被抛出时,异常安全的函数应该具备:
- 不泄漏资源。
- 不允许数据结构成为损坏的状态
从异常安全性的角度来看,这个函数很糟糕。异常安全性有两个要求,这里都不满足。
解决资源泄漏问题很容易,在条款13中我们已经解释了如何使用对象来管理资源,条款14引入了Lock类,以确保互斥量及时释放:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex); // 取得互斥量,并确保释放(见条款14)
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
具备异常安全性的函数,提供以下三种保证之一:
1、基本承诺:如果抛出异常,程序中的所有内容都保持有效状态。
2、强烈保证:如果抛出异常,程序的状态不会改变(要么全部成功,要么就像没发生过)。
3、不抛出异常(nothrow)保证:它们总是按照承诺的方式执行。内置类型(例如整型、指针等)上的所有操作都是nothrow(即提供nothrow承诺)。这是构建异常安全代码的关键组成。
下面的代码改变了bgImage的类型,使用智能指针不但可以防止资源泄漏,还提供了异常安全性;我们还改变了++imageChanges;语句的位置:
class PrettyMenu {
...
std::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); // 用“new Image”表达式的结果
// 替换bgImage的内部指针
++imageChanges;
}
shared_ptr::reset函数只有在其参数“new Image(imgSrc)”成功创建时才会被调用。delete只在reset调用中使用,所以如果从未进入该函数,那么就永远不会使用delete。
美中不足的是参数imgSrc。如果Image构造函数抛出异常,输入流的read标记可能被移动了,而这种移动对程序的其他部分来说是可见的状态变化。在changeBackground解决这个问题之前,它只能提供异常安全性的基本承诺。
另一个常用于提供强烈保证的方法是我们所提到过的 copy and swap:
struct PMImpl { // PMImpl = "PrettyMenu Impl.";
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; // 见条款25
Lock ml(&mutex); // 获取互斥量
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));// 复制obj. data
pNew->bgImage.reset(new Image(imgSrc)); // 改变副本
++pNew->imageChanges;
swap(pImpl, pNew); // 交换数据
} // 释放互斥量
- 异常安全的函数不会泄漏资源,也不允许数据结构损坏,即使在抛出异常时也是如此。这些函数提供了基本的、强的或无抛出的保证。
- 强保证通常可以通过复制-交换实现,但并不是对所有函数都适用。
- 函数提供的保证通常不会超过它调用的函数的最弱保证。