1.“异常安全性”(exception safety)能够为函数带来两个好处:
- 不泄漏任何资源;
- 不允许数据败坏:操作未正确完成却已经改变了部分数据。
以这样的思想设计的函数(异常安全性函数)提供以下三个保证,即具有三个安全性等级:
- 基本承诺:如果抛出异常,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。但程序的现实状态不可预料。
- 强烈保证:如果异常被抛出,程序状态不改变——如果函数成功,就完全成功;如果函数失败,程序会回复到调用函数之前的状态。
- 不抛掷(nothrow)保证:承诺绝不抛出异常,总能完成所承诺的功能。
应该尽量提供更高安全性的函数。
2.任何使用动态内存的东西(如STL容器)如果无法找到足够的内存以满足需求,通常便会抛出一个bad_alloc异常。
3.应当使异常安全性尽量更高并尽量等级一致。
(1)提供”不抛掷保证“:实现上当然希望有”不抛掷保证“,但是很多情况下并不现实,比如在调用纯C语言部分的函数时。
(2)提供”强烈保证“:
copy and swap方法很容易得到“强烈保证”等级的异常安全性:为计划修改的对象(原件)复制出一份副本,然后在副本上做一切必要的修改,修改成功后再将修改过的副本和原对象在一个不抛出异常的操作中转换。这样即使修改过程中抛出了异常,则原件并未改变。
但是该方法并不能保证整个函数都有强烈的异常安全性,比如函数可能调用了其他异常安全性更低(如基本承诺甚至无异常安全性)的函数,从而使该函数的异常安全性降低到更低的水平。即使相互调用的函数异常安全性水平相当,仍然可能会降低异常安全性,即所谓的”side effect“,这取决于它们所操作的数据是局部的还是全局的。这类似于”木桶原理“——相互调用的函数的异常安全性由水平最低的那个函数决定。该方法另外一个缺点是效率低下。
copy and swap方法在实现上通常采用所谓的“pimpl idiom“方法:将所有”隶属对象的数据“从原对象中放进另一个对象内,然后赋予原对象一个指针,指向那个实现对象。
(3)提供”基本保证“:当“强烈保证”不切实际时,应当提供“基本保证”。
4.系统中有一个函数不具备异常安全性,那么整个系统就不具备异常安全性。
编写代码时应确保:
- (1)以对象管理资源,以阻止资源泄漏。
- (2)挑选三个“异常安全保证”中的某一个实施于所有函数上。应当挑选现实能够保证的最强烈等级;只有当函数调用了传统代码,才别无选择地将系统设定为“无任何保证”。将这些东西写进开发文档里。
5.实例:假设有一个类用来表现带有背景图案的GUI菜单项,该类设计用于多线程环境,所以有互斥器作为并性控制:
class PrettyMenu{
public:
...
void changeBackground(std::istream& imgSrc); //改变背景图案
...
private:
Mutex mutex; //互斥器
Image* bgImage; //保存当前背景图案
int imageChange; //记录背景图案改变的次数
};
如果changeBackground()函数如下定义:
void PrettyMenu::changeBackground(std::istream& imgSrc){
lock(&mutex); //取得互斥器
delete bgImage; //删除旧的背景图案
++imageChange; //修改次数+1
bgImage = new Image(imgSrc); //添加新的背景图案
unlock(&mutex); //释放互斥器
}
这个函数版本问题很多:如果new操作抛出异常,则互斥器永远不会释放,同时图案指针就指向一个空对象,并且在修改失败的情况下次数仍然累加上去。
优化方法:(1)以对象管理资源:
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock m1(&mutex); //获得互斥器并放它放进资源管理类中
delete bgImage;
++imageChange;
bgImage = new Image(imgSrc);
//不需要再调用unlock()了
}
(2)重新排列语句次序,使得在更换图像之后才累加次数——不要为了表示某件事情发生而改变对象状态,除非那件事情真正发生了。
class PrettyMenu{
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc)); //以new的执行结果设定为bgImage的内部指针
++imageChanges;
}
由于使用了智能指针,因此不再需要手动删除new出的对象。另外,只有在new成功的情况下才会调用reset()函数;而delete只在reset()函数内被使用,如果new抛出异常,则reset()不被调用,更不会删除。
(3)使用copy and swap方法下的pimpl idiom手段:
struct PMImpl{ //使用结构而不使用类,因为PrettyMenu类的数据封装已经由私有成员的智能指针来保证,使用结构会更灵活
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu{
...
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
using std::swap;
Lock m1(&mutex); //获得互斥器的副本
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
++pNew->imageChanges;
swap(pImpl, pNew); //交换数据,释放互斥器
}