最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
假如我们在给一个GUI写一个可更换背景的菜单类。这个类用于多线程环境,所以它有个互斥锁(mutex)作为并发控制之用:
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc);
private:
Mutex mutex; //互斥锁
Image* bgImg; //当前背景图片
int imgChange; //背景被更改的次数
};
下面是 PrettyMenu
类更换背景图片的函数:
void PrettyMenu::changeBackground(std::istream& imgSrc){
lock(&mutex); //上锁
delete bgImg; //去掉老背景
++imgChange; //更新计数器
bgImg = new Image(imgSrc); //换上新背景
unlock(&mutex); //解锁
}
从“异常安全性”的角度来看,这个函数很危险。“异常安全”有两个条件,上述函数任何一个都没满足。
当异常被抛出时,带有“异常安全性”的函数会:
- 不泄漏任何资源。一旦
new Image(imgSrc)
抛出异常,unlock(&mutex)
就不会调用,这个 mutex 就永远上锁了。 - 不允许破坏数据。如果
new Image(imgSrc)
抛出异常,bgImg
就是指向一个被删除的对象,imgChange
也已被累加
解决资源泄漏的问题很容易,条款13讨论过如何以对象管理资源,而条款14也设计过 Lock
类作为一种确保互斥锁被及时释放的方法:
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock ml(&mutex); //安全的Lock类详见第14章
delete bgImg;
++imgChange;
bgImg = new Image(imgSrc);
}
在解决数据被破坏的问题前,我们来了解下异常安全的三个级别:
- 基本保证:如果抛出了异常,函数也能在有效的状态下运行,没有对象或数据损坏,所有对象也保持内部一致。然而程序本身则可能处于不确定状态。例如用户使用
changeBackground()
时抛出了异常,PrettyMenu
对象可能依然持有原来的背景,或者持有默认的背景,但具体哪个则是不确定的。 - 强烈保证:如果抛出了异常,程序的状态不会改变。这就意味着对强保证函数的调用是原子性的(atomic),如果成功了就成功了,如果失败了就像什么都没有发生一样。强保证函数比基本保证函数更容易使用,因为强保证函数只能导致两种状态,成功或者不变,而基本保证的函数可能处于任何状态——只要那是个合法状态。
- 不抛出保证:保证永远不会抛出异常。例如所有对于内置类型(int,指针等等)的操作都提供不抛出保证。它是异常安全代码中一个必不可少的关键基础材料。
如果我们假设,不给函数规定抛出什么异常,即写一个空的异常规范(empty exception specification),那它不就能提供最安全的不抛出保证了吗?
int doSomething() throw(); //空的异常规范
这并不是说 doSomething()
绝不会抛出异常,而是说如果它抛出异常,将是严重错误,然后触发 unexpected()
函数,而 unexpected()
默认会触发 terminate()
,实际上doSomething()
也没有提供任何异常保证。异常安全取决于函数的实现,而不是函数的声明。
异常安全的代码必须提供上述三种保证之一,不然它就不具备异常安全性。除非要用到异常不安全的老代码,否则一定要照顾到代码的异常安全性。当然,盲目追求最好也是不切实际的,不抛异常虽完美但显然很难做到。任何使用动态分配内存的东西(例如STL容器)都会在内存不足时抛出 bad_alloc
异常。所以可行的时候提供不抛出保证,大多数情况下还是要在基本和强保证中做出选择。
对 changeBackground()
而言,提供强烈保证几乎不难。首先把 PrettyMenu
的 bgImg
成员变量的类型从 Image*
改为用智能指针存储;然后重新排列changeBackground()
内的语句顺序,使得背景更换之后再累加 imgChange
。
class PrettyMenu{
...
shared_ptr<Image> bgImg; //使用shared_ptr
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); //替换新图片
++imgChange; //完成后再更新计数器
};
注意,这里不需要 delete 旧背景,因为智能指针已经帮你完成了。此外,删除动作只发生在新背景被成功创建之后,即 shared_ptr::reset()
只有在其参数(new Image(imgSrc)
的执行结果)被成功生成后才会被调用,而 delete 在 reset()
函数内被执行。
前面说提供强烈保证几乎不难。如果 istream&
类型的它突然变得不可读了,Image
类的构造函数则可能会抛出异常,这种情况我们还没有照顾到,所以 changeBackground()
在解决这个问题之前只提供基本保证。
有个一般化的设计策略能够提供强烈保证,这个策略是先拷贝再交换(见条款11),原理很简单:拷贝你想要改变的对象,然后对拷贝对象执行操作,如果其中任何一步抛出异常,对象本体不会变。当成功执行完所有操作时,将拷贝对象和本体在一个不抛出异常的操作中交换。
实现上通常是将所有“属于对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法称为 pimpl idiom(条款25有提到,条款31详细描述)。对 PrettyMenu
而言,典型写法如下:
struct PMImpl{ //PrettyMenuImplementation
shared_ptr<Image> bgImage;
int imgChange;
};
class PrettyMenu{
...
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
using std::swap; //这句见条款25
Lock ml(&mutex); //使用条款14安全管理mutex的Lock类,上锁
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //生成数据的临时拷贝
pNew->bgImg.reset(new Image(imgSrc)); //在拷贝上执行操作,用std::shared_ptr::reset替换图片
++pNew->imgChange;
swap(pImpl,pNew); //调换回去
}//解锁
PMImpl
之所以使用 struct 而不是 class ,是因为 PrettyMenu
的数据封装性已经由 pImpl
处于 private 域而获得了保证。当然也可以用 class ,但有时候却不太方便。
copy and swap 策略是实现原子性操作(atomicity)——成功操作或者不做任何操作的一个好方法,但一般而言它并不保证整个函数有强烈的异常安全性。为了解原因,看下面的代码:
void someFunc(){
...//生成拷贝
f1();
f2();
...//把修改后的状态交换过来
}
如果 f1()
或f2()
的异常安全性比“强烈保证”低,就很难让 someFunc()
成为“强烈异常安全”。举个例子,假设 f1()
只提供基本保证,那么为了让 someFunc()
提供强烈保证,我们必须获得调用 f1()
之前的整个程序状态,捕捉 f1()
的所有可能的异常,然后恢复原状态。
可是就算 f1()
和 f2()
能提供强保证,情况并不就此好转。设想假如f1()
操作成功,程序状态再任何方面都可能有所改变,然后f2()
抛出异常,这种情况下我们就需要把 f1()
所做的改动也撤回。可是有些改动则是根本不能撤回的,例如对数据库已提交的(committed)改动,用户早就已经看到了。这些问题都会导致你对强烈保证望而却步,再加上 swap and copy本身也有效率问题,因为要生成拷贝,对拷贝对象执行操作,再替换原来的对象,这是一个费时费空间的操作。所以,不用什么时候都需要提供强烈保证,但你有能力实现时可以提供强烈保证。
当强烈保证不切实际时,你就必须提供基本保证。如果你实现了强烈保证,但付出的代价是巨大的,你可以退一步选择基本保证。对许多函数来说,“异常安全性之基本保证”是一个绝对通情达理的选择。
要设计异常安全的系统,首先从堵住资源泄漏开始,使用资源管理类,然后在这三种异常安全性中做出符合实际的选择,只有在使用异常不安全的老代码让你无路可走时才考虑零保证,最后记录下你的决定和原因,为用户和以后的维护考虑。
Note:
- 异常安全的函数即使在抛出异常时也不会泄露资源,损坏数据结构。这种安全性有三种级别,基本保证,强列保证和不抛出保证
- copy and swap是实现强保证的有效方法,但强烈保证并不是每个函数都可实现或具备实现意义的
- 函数的异常安全性遵循木桶原理,函数的最强安全性取决于它所调用操作的最弱安全性