14 审慎的使用exception specifications

编译器在编译的时有时能检测到异常规格的不一致,而且一个异常函数抛出一个不在异常规格范围内的异常,系统在运行时能够检测到这个错误,然后特殊函数unexpected将被自动调用,终止程序,导致一场灾难。

 

我们很容易编写出这种导致灾难的函数。编译器仅仅部分的检测异常的使用是否与异常规格一致。一个函数调用另外一个函数 ,并且后者可能抛出一个违反前者的异常规格的异常,(A函数调用了B函数,但B函数可能抛出一个不在A函数异常规格之类的异常,所以这个函数就违反了A函数的异常规格)编译器不对此种情况进行检测,并且语言标准也禁止了编译器拒绝这种方式的调用(尽管显示警告)

例如:

 

extern void f1();	//可以抛任何异常

void f2() throw(int);//只能抛出int异常

void f2() throw(int)
{
	...
	f1();	//即使f1可能抛出不是int类型的异常也是合法的
	...
}

编写代码的时候尽量减少这种行为,有可能会导致异常终止。

 

第一种方法避免在带有异常类型参数的模板内使用异常规格。

例如:

template
bool operator == (const T& lhs,const T& rhs) throw()
{
	return &lhs == &rhs;
}

这个模板包含的异常规格表示模板生成的函数不能抛异常,但是事实可能不会这样,因为operator&能被一些类型对象重载。如果被重,operator&可能抛异常,这就违反了我们的异常规格,使程序可能会调用unexpected,最终导致程序终止。

 

上述的例子是一种更一般的特例,这个更一般问题也就是我们无法知道某种模板类型参数抛出什么异常。我们几乎不可能为一个模板提供一个有意义的的异常规格。因为模板总是采用不同的方法是用类型参数。解决方法就是不要让模板和异常规格混用。

 

能够避免unexpected的第二种方法是如果一个函数内部调用了没有异常规格的函数时,这个函数应该去掉本身的异常规格。但现实中容易被忽略:

 

typedef void(*CallBackPtr)(int eventXLocation,
						   int eventYLocation,
						   void *dataToPassBack);//回调函数
						   
class CallBack
{
public:
	CallBack(CallBackPtr* fPtr,void* dataToPassBack)
		:func(fPtr),data(dataToPassBack){}
		
	void makeCallBack(int eventXLocation,int eventYLocation) const throw();
private:
	CallBackPtr* func;
	void* data;
};
	
void CallBack::makeCallBack(int eventXLocation,int eventYLocation) const throw()
{
	func(eventXLocation,eventYLocation,data);
}

这里makeCallBack内调用func,有可能违反异常规格的风险,因为func可能会抛异常。

通过在typedef中采用异常规格来解决问题:

typedef void(*CallBackPtr)(int eventXLocation,
						   int eventYLocation,
						   void *dataToPassBack)  throw();//回调函数

这样定义了typedef后,如果注册了一个可能会抛异常的callback函数将是非法的:

void CallBackFcn1(int eventXLocation,int eventYLocation,
						void *dataToPassBack);
						
void* callBackData;
...
CallBack(CallBackFcn1,callBackData);	//错误,CallBackFcn1可能抛异常

//带有异常规格的回调函数	
void CallBackFcn1(int eventXLocation,int eventYLocation,
						void *dataToPassBack) throw();
						
CallBack(CallBackFcn1,callBackData) ;	//正确

 

避免调用unexpected的第三种方法是处理系统本身抛出的异常。这种异常最常见的就是bad_alloc,当内存分配失败的时候它被operator new和operator new[]抛出。

C++允许你使用其他不同的异常类型替换unexpected异常,例如:

 

 

class UnexpectedException{};	//所有的unexpected异常都被这个类替换

void covertUnexpected()			//如果一个unexpected异常抛出,这个函数被调用
{
	throw UnexpectedException();
}

通过covertUnexpected替换缺省的unexpected函数:

set_unexpected(covertUnexpected);

 

另外一种做法把unexpected异常转换成知名的类型方法来替换unexpected,让其重新抛出当前的异常,这样的异常可以被替换成bad_exception。代码如下:

 

void covertUnexpected()	//如果一个unexpected异常被抛出,这个函数只继续抛出当前异常
{
	throw;
}

set_unexpected(covertUnexpected);	//安装covertUnexpected替换unexpected异常

这样做所有异常将被替换成bad_exception,这个异常替代原来的异常继续传递。

 

异常还有还有一个缺点就是它们能导致unexpected被触发,即使一个high-level调用者准备处理抛出的异常:

class Session
{
public:
	~Session();
	...
private:
	static void logDestruction(Session* objAddr) throw();
};


Session::~Session()
{
	try{
		logDestruction(this);
	}
	catch(...)
	{
		
	}
}

 

上述代码假如logDestruction调用的函数抛出异常,logDestruction没捕获,而logDestruction却是一个不抛异常规格的函数,导致unexpected函数被调用。即使作者想处理所有的异常,所以不应该不给Session析构函数的catch块执行的机会就终止程序了。所以将logDestruction异常规格去掉。

 

 

综上所述,异常规格是一个应被审慎使用的特性。在把它们加入到你的函数之前,应该考虑它们带来的行为是否是你希望的。

 

 

 

你可能感兴趣的:(More,Effective,C++读书笔记,More,Effective,C)