毫无疑问,异常规格是一个引人注目的特性。它使得代码更容易理解,因为它明确地描述了一个函数可以抛出什么样的异常。但是它不只是一个有趣的注释。编译器在编译时有时能够检测到异常规格的不一致。而且如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。异常规格既可以做为一个指导性文档同时也是异常使用的强制约束机制,它好像有着很诱人的外表。
不过在通常情况下,美貌只是一层皮,外表的美丽并不代表其内在的素质。函数unexpected缺省的行为是调用函数terminate,而terminate缺省的行为是调用函数abort,所以一个违反异常规格的
程序其缺省的行为就是halt(停止运行)。在激活的stack frame中的局部变量没有被释放,因为abort在关闭
程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。
不幸的是,我们很容易就能够编写出导致发生这种灾难的函数。编译器仅仅部分地检测异常的使用是否与异常规格保持一致。一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常,(A函数调用B函数,因为B函数可能抛出一个不在A函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格 译者注)编译器不对此种情况进行检测,并且语言标准也禁止它们拒绝这种调用方式(尽管可以显示警告信息)。
例如函数f1没有声明异常规格,这样的函数就可以抛出任意种类的异常:
| extern void f1(); // 可以抛出任意的异常 |
假设有一个函数f2通过它的异常规格来声明其只能抛出int类型的异常:
f2调用f1是非常合法的,即使f1可能抛出一个违反f2异常规格的异常:
void f2() throw(int) { ... f1(); // 即使f1可能抛出不是int类型的 //异常,这也是合法的。 ... } |
当带有异常规格的新代码与没有异常规格的老代码整合在一起工作时,这种灵活性就显得很重要。
因为你的编译器允许你调用一个函数其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的
程序执行被终止,所以在编写
软件时采取措施把这种不一致减小到最少。一种好方法是避免在带有类型参数的
模板内使用异常规格。例如下面这种
模板,它好像不能抛出任何异常:
// a poorly designed template wrt exception specifications template<class T> bool operator==(const T& lhs, const T& rhs) throw() { return &lhs == &rhs; } |
这个
模板为所有类型定义了一个操作符函数operator==。对于任意一对类型相同的对象,如果对象有一样的地址,该函数返回true,否则返回false。
这个
模板包含的异常规格表示
模板生成的函数不能抛出异常。但是事实可能不会这样,因为opertor&(地址操作符)能被一些类型对象重载。如果被重载的话,当调用从operator==函数内部调用opertor&时,opertor&可能会抛出一个异常,这样就违反了我们的异常规格,使得
程序控制跳转到unexpected。
上述的例子是一种更一般
问题的特例,这个
问题也就是没有办法知道某种
模板类型参数抛出什么样的异常。我们几乎不可能为一个
模板提供一个有意义的异常规格。,因为
模板总是采用不同的方法使用类型参数。解决方法只能是
模板和异常规格不要混合使用。
能够避免调用unexpected函数的第二个方法是如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格。这很容易理解,但是实际中容易被忽略。比如允许用户注册一个回调函数:
// 一个window系统回调函数指针 //当一个window系统事件发生时 typedef void (*CallBackPtr)(int eventXLocation,int eventYLocation,void *dataToPassBack); //window系统类,含有回调函数指针, //该回调函数能被window系统客户注册 class CallBack { public: CallBack(CallBackPtr fPtr, void *dataToPassBack): func(fPtr), data(dataToPassBack) {} void makeCallBack(int eventXLocation, int eventYLocation) const throw(); private: CallBackPtr func; // function to call when // callback is made void *data; // data to pass to callback }; // function // 为了实现回调函数,我们调用注册函数, //事件的作标与注册数据做为函数参数。 void CallBack::makeCallBack(int eventXLocation,int eventYLocation) const throw() { func(eventXLocation, eventYLocation, data); } |
这里在makeCallBack内调用func,要冒违反异常规格的风险,因为无法知道func会抛出什么类型的异常。
通过在
程序在CallBackPtr typedef中采用更严格的异常规格来解决
问题:
typedef void (*CallBackPtr)(int eventXLocation,int eventYLocation, void *dataToPassBack) throw(); |
这样定义typedef后,如果注册一个可能会抛出异常的callback函数将是非法的:
// 一个没有异常给各的回调函数 void callBackFcn1(int eventXLocation, int eventYLocation, void *dataToPassBack); void *callBackData; ... CallBack c1(callBackFcn1, callBackData); //错误!callBackFcn1可能 // 抛出异常 //带有异常规格的回调函数 void callBackFcn2(int eventXLocation, int eventYLocation, void *dataToPassBack) throw(); CallBack c2(callBackFcn2, callBackData); // 正确,callBackFcn2 // 没有异常规格 |
传递函数指针时进行这种异常规格的检查,是语言的较新的特性,所以有可能你的编译器不支持这个特性。如果它们不支持,那就依靠你自己来确保不能犯这种错误。
避免调用unexpected的第三个方法是处理系统本身抛出的异常。这些异常中最常见的是bad_alloc,当内存分配失败时它被operator new 和operator new[]抛出。如果你在函数里使用new操作符,你必须为函数可能遇到bad_alloc异常作好准备。
现在常说预防胜于治疗(即做任何事都要未雨绸缪 译者注),但是有时却是预防困难而治疗容易。也就是说有时直接处理unexpected异常比防止它们被抛出要简单。例如你正在编写一个软件,精确地使用了异常规格,但是