Effective C++条款29:为“异常安全”而努力是值得的(Strive for exception-safe code)
- 条款29:为“异常安全”而努力是值得的
- 1、抛出异常的案例
- 2、解决资源泄露的问题
- 3、异常安全的三种保证
- 4、两种解决异常安全的方法
- 4.1 使用智能指针
- 4.2 拷贝和交换
- 5、牢记
- 总结
《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:
第5章:实现
条款29:为“异常安全”而努力是值得的
1、抛出异常的案例
假设我们有一个表示GUI菜单的类,这个GUI菜单有背景图片。这个类将被使用在多线程环境中,所以需要mutex进行并发控制。
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc) // 改变背景图像
...
private:
Mutex mutex; //互斥器
Image* bgImage; //目前使用的背景图片
int imageChanges;//图片被修改的次数
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
lock(&mutex); //取得互斥器
delete bgImage; //删除旧图片
++imageChanges; //修改图像更改次数
bgImage = new Image(imgSrc); //安装新的背景图片
unlock(&mutex); //释放互斥器
}
上面的changeBackground()成员函数不是“异常安全的”。因为异常安全的函数应该有以下两种特性:
- ① 不泄露任何资源:上述的代码如果new Image()操作导致异常,那么就永远不会调用unlock,那么互斥器将永远被锁住。
- ② 不允许数据破坏:如果new Image()操作导致异常,那么bgImage已经被删除了,而且imageChanges数量也被累加了,所以资源被改变了。
2、解决资源泄露的问题
这个问题很容易解决,在条款13中讨论了如何以对象管理资源,条款14也介绍了自己设计一个名为Lock的类来管理互斥器,定义如下:
//修改后PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);//将互斥器封装在类中进行管理
delete bgImage; //删除旧图片
++imageChanges; //修改图像更改次数
bgImage = new Image(imgSrc); //安装新的背景图片
}
使用像Lock一样的资源管理类的一个极大的好处是它通常使函数更短。比如为什么不再需要对unlock的调用了?作为一个通用的规则,代码越少越好,因为对代码做改动时,出错和理解错误的可能性变低了。
3、异常安全的三种保证
接下来让我们看一看数据结构被损坏的问题。这里我们要做出选择,但是在我们可以进行选择之前,必须对定义这些选择的术语做一下比较。
异常安全的函数提供了如下三种保证的一种:
- ① 基本承诺:如果异常被抛出,程序内的任何事物都应该保持在有效状态。
没有对象或者数据被损坏,并且所有对象或者数据保持一个内部一致的状态(例如所有类的约束条件继续被满足)。然而,程序的正确状态可能不能够被预测出来。例如,我们可以实现changeBackground,一旦异常被抛出,PrettyMenu对象可以继续拥有旧的背景图片,但是客户不能够预测拥有的是哪一个。(为了能够找出这个图片,大概需要调用一些成员函数,来告诉它们当前的背景图片是哪一个)
- ② 强烈保证:如果程序抛出异常,程序状态不应该保证。调用这样的函数应该保证:如果函数成功就是完全成功;如果函数执行失败,程序会恢复到“调用函数之前”的状态。
使用提供强保证的函数比只提供基本保证的函数要更加容易,因为调用提供强保证的函数之后,只可能有两种程序状态:函数被正确执行后的状态,或者函数被调用之前的状态。而如果在调用只提供基本保证的函数的时候抛出异常,程序可以进入任何有效状态。
- 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。
所有在内置类型上进行的操作都是无异常的。这是异常安全代码的一个关键的构建基础。
认为带有空异常明细(empty exception specification)的函数是无异常的,这可能看上去是合理的,但事实上不是这样。举个例子,如:
int doSomething() throw();//空白的异常明细
这并不是说doSomething永远不会抛出异常。它的意思是如果soSomething抛出异常,就会是一个严重的错误,并且会调用意料不到的函数。事实上,doSomething没有提供任何异常安全保证。这个函数的声明(如果有异常明细,也包含异常明细)并没有告诉你这个函数是否是正确的,可移植的或者效率高的,也没有为你提供任何异常安全保证。所有这些特性都由函数的实现来决定,而不是声明。
NOTE:异常安全的代码必须提供上面三种保证的一种。如果没有提供,它就不是异常安全的。
4、两种解决异常安全的方法
4.1 使用智能指针
对于changeBackground来说,提供强烈保证不是多难的事。
首先,我们将PrettyMenu的bgImage数据成员的类型从内建的Image*指针替换为一种资源管理智能指针(见条款13)。说真的,对于防止资源泄露来说这绝对是一个好方法。它帮我们提供强异常安全保证的事实只是简单对条款13中的论述(使用对象管理资源是好的设计的基础)做了进一步的加强。在下面的代码中,我将会展示tr1::shared_ptr的使用,因为当进行拷贝时使用tr1::shared_ptr比使用auto_ptr更加直观,更受欢迎。
第二,我们对changeBackground中的语句进行重新排序,达到只有image被修改的时候才会增加imageChnages的目的。作为通用准则,一个对象的状态没有被修改就表明一些事情没有发生。
class PrettyMenu {
...
private:
std::tr1::shared_ptr<Image> bgImage;
};
//修改后的PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc));//以"new Image"的执行结果
++imageChanges;//设定bgImage内部指针
}
注意这里不再需要手动delete旧图像,因为这由智能指针在内部处理。并且,销毁操作只有在新image成功创建的时候才会发生。更精确的说,只有在参数(new Image(imgSrc)的结果)被成功创建的时候tr1::shared_ptr::reset函数才会被调用。delete只在reset函数内部被使用,所以如果reset不被调用,delete永远不会被执行。注意资源管理对象的使用再次削减了changeBackground的长度。
正如我所说的,上面的两个修改足以为changeBackground提供强异常安全保证。还有美中不足的就是关于参数imgSrc。如果Image的构造函数抛出异常,输入流的读标记可能会被移动,这个移动致使状态发生变化并且对程序接下来的运行是可见的。如果changeBackground不处理这个问题,它只能提供基本异常安全保证。
4.2 拷贝和交换
“3”中只解决了基本保证,但在changeBackground()函数中,Image的构造函数可能会抛出异常,也就是无法提供强烈保证。有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略就是copy and swap。
copy and swap策略的原则是:为你打算修改的对象(原件)做一份副本,然后在副本身上做修改:
-
如果在副本的身上修改抛出了异常,那么原对象未改变状态。
-
如果在副本的身上修改未抛出异常,那么就将修改过的副本与原对象进行置换(swap)。
实际上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(即副本)。这种收发被称为“pimpl idiom”,在条款31中会进行描述。对于上面的PrettyMenu来说,典型的写法如下:
struct PMImpl {//将bgImage和imageChanges从PrettyMenu独立出来,封装成一个结构体
std::tr1::shared_ptr<Image> bgImage;
int imageChanges
};
class PrettyMenu {
//...
private:
std::tr1::shared_ptr<PMImpl> pImpl; //创建一个该结构
};
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);
}
在这个例子中,我选择将PMImpl定义为一个结构体而不是类,因为PrettyMenu的封装性通过pImpl的私有性(private)来保证。把PMImpl定义成类会至少和结构体一样好,虽然有一些不方便。如果需要,PMImpl可以被放在PrettyMenu中,但是打包问题(packaging)是我们所牵挂的。
5、牢记
-
异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。
-
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
-
函数提供的“异常安全保证”通常最高值等于其所调用之各个函数的“异常安全保证”中的最弱者。
总结
期待大家和我交流,留言或者私信,一起学习,一起进步!