为“异常安全”而努力是值得的

一.异常安全函数(Exception-safe function)即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。

1)基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态,没有任何对象或数据结构会因此而败坏,所有对象都处于一个内部前后一致的状态(例如所有的classes约束条件都继续获得满足)。然后程序的现实状态(exact state)恐怕不可预料。举个例子,我们可以撰写changeBackground使得一旦有异常被抛出时,PrettyMenu对象可以继续拥有原背景图像,或是令他拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。

2)强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到”调用函数之前“的状态。

      和这种提供保证的函数共事,比和刚才说的那种只提供基本承诺的函数共事,容易多了,因为在调用一个提供强烈保证的函数后,程序状态只有两种可能:如逾期地到达函数成功执行后的状态,或回到函数被调用前的状态。与此成对比的是,如果调用一个只提供基本承诺的函数,而真的出现异常,程序有可能处于任何状态-只要那是个合法状态。

3)不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如 int,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

     如果我们假设,函数带着”空白的异常明细“(empty exception specification)者必为nothrow函数,似乎合情合理,其实不尽然。举个例子,考虑一下下函数:

int doSomething()  throw();     //注意”空白的异常明细“

    这并不是说doSomething绝不会抛出异常,而是说如果doSomething抛出异常,将是严重错误,会有你意想不到的函数被调用。实际上doSomething也许完全没有提供任何异常保证。函数的声明式(包括其异常明细-如果有的话)并不能告诉你是否它是正确的、可移植的或高效的,也不能告诉你它是否提供任何异常安全性保证。所有那些性质都由函数的实现决定,无关乎声明。


二.”强烈保证“往往能够以copy-and-swap实现出来,但”强烈保证“并非对所有函数都可实现或具备现实意义。

1)策略copy and swap 的原则很简单:为你打算修改的对象(原件)做出一份副本,然后再那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。带所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。

2)实现上通常是将所有”隶属对象的数据“从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象。这种手法常被称为pimpl idiom,条款31详细描述了它。

假设有个class用来夹带背景图案的GUI菜单,这个class希望用于多线程环境,所有它有个互斥器(mutex)作为并发控制(concurrency control)之用。

对PrettyMenu而言,典型写法如下:

class Uncopyable
{
public:
	Uncopyable() {}
	~Uncopyable(){}
private:                                 //禁止copying    
	Uncopyable(const Uncopyable&);
	Uncopyable& operator = (const Uncopyable&);
};

class Lock:private Uncopyable  //Lock RAII
{
public:
	explicit Lock(Mutex* pm):mutexPtr(pm)
	{ lock(mutexPtr); }
	
	~Lock(){ unlock(mutexPtr); }
private:
	Mutex* mutexPtr;
};

struct PMImpl {                                   //PMImpl="PrettyMenuImpl"
	std::tr1::shared_ptr<Image>bgImage; 
	int imageChanges;
};

class PrettyMenu {
public:
	void changeBackground(std::istream& imgSrc)
private:
	Mutex mutex;
	std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	using std::swap;                         //见条款25
	Lock m1(&mutex);                         //获得mutex的副本数据
	std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  

	pNew->bgImage.reset(new Image(imgSrc));  //修改副本
	++pNew->imageChanges;

	swap(pImpl,pNew);                        //置换(swap)数据,释放mutex
}

3)copy-and-swap的关键在于”修改对象数据的副本“,然后在一个不抛异常的函数中将修改后的数据和原件置换”,因此必须为每一个即将被改动的对象做出一个副本,那得耗用你可能无法(或无意愿)供应的时间和空间。是的,大家都希望提供“强烈保证”;当它可被实现时你得去应该提供它,但“强烈”保证并非在任何时刻都显得实际。


三.函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

        这令我想到怀孕。一位女性若非怀孕,就是没怀孕。不可能说她“部分怀孕”。同样道理,一个软件系统要不就具备异常安全性,要不就全然否定,没有所谓的“局部异常安全系统”。如果系统内有一个(惟有一个)函数不具备异常安全性,整个系统就不具备异常安全性,因为调用那个(不具备异常安全性的)函数有可能导致资源泄露或数据结构败坏。不幸的是很多老旧C++代码不具备异常安全性,所以今天很多系统仍然不能够说是“异常安全”的,因为他们并入了一些并非“异常安全”的代码。

        没有理由让我这种情况永垂不朽。当你撰写新码或修改旧码时,请仔细想想如何让它具备 安全性。首先是“以对象管理资源(条款13)”,那可阻止资源泄露。然后挑选三个“异常安全保证”中的某一个实施于你所写的每一个函数身上,你应该挑选“现实可施作”条件下的最强烈等级,只有当你的函数调用了传统代码,才别无选择地将它设为“无任何保证”。将你的决定写成文档,这一来是为你函数用户着想,二来是为将来的维护者着想。函数的“异常安全性保证”是其可见接口的一部分,所以你应该慎重选择,就像选择函数接口的任何其他部分一样。

你可能感兴趣的:(为“异常安全”而努力是值得的)