14.审慎使用异常明细

(本节内容在Effective C++ 30中亦有涉及)

前言

 
异常明细明确描述了一个函数可能抛出的异常,如果说函数抛出了一个不能存在于异常明细范围里的异常,系统将在运行时检测到该错误,并且自动地调用特殊函数unexpected。
unexpected默认机制是调用terminate,而terminate缺省行为是调用abort.也就是说,一旦违反了异常明细,程序将立刻终止,所有资源(甚至包括局部变量)都未得到释放。


问题实例

 
前言中描述的情况其实极易发生,因为编译器仅仅部分地检测异常的使用是否与异常明细保持一致。举例而言,假设函数A有一个明确的异常明细,并且调用了函数B,函数B抛出了违反函数A异常明细的异常,所以本次函数调用就违反了A的异常明细,程序将立即终止:

1
2
3
4
5
6
extern void f1();//可以抛出任意的异常
void f2() throw(int){//异常明细中只有int
...
f1(); //抛出非int型异常,程序终止
...
}


解决方案

避免在带有类型参数的模板中使用异常

1
2
3
4
template<class T>
bool operator==(const T& lhs, const T& rhs) throw(){
return &lhs == &rhs;
}

这个函数的意思是,如果类型相同的两个对象地址相同,则认为它们相等。看起来好像确实不会抛出异常,但实际上…万一某个类重载了operator &,并且该函数还能抛异常,那我们就等着terminate吧。

事实上,我们几乎不可能为一个模版提供一个有意义的异常明细,因为模版总是采用不同的方法使用类型参数,解决方法只能是模板和异常明细不要混用。


去除异常明细

不触发terminate的另一个方法是:如果在一个具备异常明细的函数内调用其它没有异常明细的函数,那么我们应该去除该函数的异常明细。

实例

允许用户注册一个回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一个 window 系统回调函数指针
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);
}

显然,func可能抛出异常,违反了第二种解决方案。


处理系统本身抛出的异常

 
有人说未雨绸缪强过临阵磨枪,但实际上对于unexpected而言,直接处理unexpected异常比防止它们被抛出更有效果。
虽然阻止抛出unexpected异常是不现实的,但是C++允许以其它不同的异常类型替换unexpected异常。比如我们可以将所有unexpected异常都被替换为UnexpectedException对象:

1
2
3
4
5
class UnexpectedException {};
void convertUnexpected() {
throw UnexpectedException();
}
set_unexpected(convertUnexpected);//将unexpected替换掉

当然还有一个更安全的方法:
1
2
3
4
void convertUnexpected(){
throw;
}
set_unexpected(convertUnexpected);//unexpected被抛出时只是重新抛出当前异常


总结

 
异常明细是一个应被审慎使用的特性。在编写之前,确保你已经考虑了所有可能发生的情况。