1 异常限定符与unexpected调用
如下面的代码所示,标识符throw()即为异常限定符。异常限定符标识了函数可以抛出的异常类型。当throw后面的括号内容为空,表示该函数不抛出任何异常。
class Exception {
public:
const char* what() throw() const; // 该函数不抛出任何异常
void doSomething() throw(int, Widget); // 该函数只抛出int和Widget类型的异常
}
throw()限定符以文档的形式,规定了函数可以抛出异常类型的范围。但是,多数编译器允许throw()修饰的函数,调用没有throw()修饰的函数。如下面的例子所示。
void func0() {
// may throw some exception;
}
void func1() throw() {
func0();
}
在运行时期,若func0()抛出异常,则特殊函数unexpected会被调用,该函数调用terminate函数,terminate默认行为会调用abort。abort停止程序运行,而程序中的局部变量无法被销毁。
2 避免策略
编译器允许带有“异常限定符”的函数调用没有“异常限定符”的函数,这种做法极具弹性,但会带来程序被迫终止,内存泄漏等问题。下面将会讨论如何避免unexpected函数被调用。
1 不要用“异常限定符throw()”修饰带有模板参数的函数
如下面代码。对于判断“==”的操作不会出现异常。但是我们无法确定,取地址操作符“&”是否已经被重载,且可能抛出异常。
此种情况的实质是,我们无法确定,所有类对象的同名函数都不会抛出异常。
template<class T>
bool operator==(const T& left, const T& right) throw() {
return &left == &right;
}
2 外层函数不使用throw()进行修饰
若被调用的内层函数B没有throw()修饰,则外层的调用函数A也不要有throw()修饰——我们无法确定函数B的的确确不会抛出异常。
我们常常会忽略的一种情况是:注册“回调函数”。如下面代码所示,如果注册的“回调函数”没有throw修饰,而调用“回调函数”的外层函数却有throw修饰,“回调函数”抛出异常就会引起程序终止。
typedef void (*CallbackPtr)();
class Callback {
public:
Callback(CallbackPtr func) : m_func(func) {}
void doSomething() throw() {
m_func(); // 可能抛出异常
}
private:
CallbackPtr m_func;
}
由于较新的编译器支持typedef后加入throw进行修饰。因此我们可以定义如下函数指针类型。但是,有可能CallbackPtr所指向的函数依然会抛出异常。因此最好还是采用本节最开始的主张——如果不确定内层函数是否会抛出异常,那么外层函数也不要用throw限制。
typedef void (*CallbackPtr)() throw();
void func0();
void func1() throw();
Callback object0(func0); // 错误,func0没有throw修饰
Callback object1(func1); // 正确,func1有throw修饰
3 自定义unexpected函数
上面提到当throw()修饰的函数抛出异常,则系统会自动调用函数unexpected(),unexpected()函数会调用terminate(),terminate()继续调用abort(),进而终止函数程序。
新的思路是,当出现问题时,抛出自定义函数,而不是调用unexpected()函数。
C++提供了函数set_unexpected(),向该函数中传递我们自定义的函数,来替换默认的unexpected()。如下面的例子所示。自定义函数抛出我们自定义的异常,从而进一步处理。
class UnexpectedException {
}
void unExpected() {
throw UnexpectedException();
}
set_unexpected(unExpected);
这样,未知异常就变成了已知异常,方便捕获和进一步处理。
3 异常的成本
若在编译过程中加入异常,就会需要额外的数据结构来记录try…catch结构。而代码、运行速度也会整体下降5%-10%。
同时,若异常没有妥善处理,则会造成程序崩溃,退出等情况发生。因此,使用异常的原则是:如果能使用参数传递、返回值,就尽量减少对异常的使用。异常只是出现在很少的一部份内。