More Effective C++ 14:审慎使用异常规格

函数unexpected 默认的行为是调用函数 terminate,而 terminate 默认的行为是调用函数 abort,所以一个违反异常规格的程序其默认的行为就是停止运行。在激活的栈中的局部变量没有被释放,因为abort在关闭程序时不进行这样的清除操作。对异常规格的触犯变成了一场并不应该发生的灾难。


不幸的是,我们很容易就能够编写出导致发生这种灾难的函数。编译器仅仅部分地检测异常的使用是否与异常规格保持一致。一个函数调用了另一个函数,并且后者可能抛出一个违反前者异常规格的异常
比如:A函数调用B函数,但因为B函数可能抛出一个不在 A 函数异常规格之内的异常,所以这个函数调用就违反了A函数的异常规格。
如:

extern void f1();
//假设有一个函数f2通过它的异常规格来声明其只能抛出int类型的异常:
void f2()throw(int);
//f2 调用 f1 是非常合法的,即使 f1 可能抛出一个违反 f2 异常规格的异常:
void f2()throw(int)
{
	...
	f1();//即使 f1 可能抛出不是int类型的异常也是合法的
	...
}

当带有异常规格的新代码与没有异常规格的老代码整合在一起工作时,这种灵活性就显得很重要。

因为你的编译器允许你调用一个函数,其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的程序执行被终止,所以在编写软件时应该采取措施把这种不一致减小到最少。一种好方法是避免在带有类型参数的模板内使用异常规格。例如下面这种模板,它好像不能抛出任何异常:

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

这个模板为所有类型定义了一个操作符函数 operator==对于任意一对类型相同的对象,如果对象有一样的地址,该函数返回 true,否则返回 false

这个模板包含的异常规格表示模板生成的函数不能抛出异常。但是事实可能不会这样,因为opertor&能被一些类型对象重载。如果被重载的话,当调用从operator==函数内部调用 opertor&时,opertor&可能会抛出一个异常, 这样就违反了我们的异常规格,使得程序控制跳转到unexpected.


我们几乎不可能为一个模板提供一个有意义的异常规格。因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用.



避免调用unexpected函数的第二个方法是如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格.
比如:

//一个 window 系统回调函数指针 
//当一个 window 系统事件发生时
//该回调函数能被 window 系统客户注册
using CallBackPtr = void(*)(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 noexcept; 
private: 
	CallBackPtr func; 
	void *data; 
};
void CallBack::makeCallBack(int eventXLocation, int eventYLocation) const noexcept 
{ 
	func(eventXLocation, eventYLocation, data); 
}
这里在 makeCallBack 内调用 func,要冒违反异常规格的风险,因为无法知道func会抛出什么类型的异常。

通过在程序在CallBackPtr using中采用更严格的异常规格来解决问题:

using CallBackPtr = void(*)(int eventXlocation, int eventYLocation, void* dataToPassBack) noexcept;

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

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

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

避免调用unexpected的第三个方法是处理系统本身抛出的异常.
这些异常中最常见的是bad_alloc,当内存分配失败时它被 operator newoperator new[]抛出.如果你在函数里使用 new 操作符,你必须为函数可能遇到bad_alloc 异常作好准备

虽然防止抛出unexpected异常是不现实的,但是 C++允许你用其它不同的异常类型替换 unexpected 异常,你能够利用这个特性。例如你希望所有的 unexpected 异常都被替换为UnexpectedException 对象。你能这样编写代码:

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

当你这么做了以后,一个 unexpected 异常将触发调用 convertUnexpected 函数。Unexpected 异常被一种 UnexpectedException 新异常类型替换.



另一种把 unexpected 异常转变成知名类型的方法是替换 unexpected 函数,让其重新抛出当前异常,这样异常将被替换为 bad_exception。你可以这样编写:

扫描二维码关注公众号,回复: 11338879 查看本文章
void convertUnexpected() // 如果一个unexpected异常被抛出,这个函数被调用  
{ 
	throw; // 它只是重新抛出当前异常 
}  
set_unexpected(convertUnexpected);

如果这么做,你应该在所有的异常规格里包含bad_exception
你将不必再担心如果遇到 unexpected 异常会导致程序运行终止。任何不听话的异常都将被替换为bad_exception,这个异常代替原来的异常继续传递。

猜你喜欢

转载自blog.csdn.net/qq_44800780/article/details/106613802
今日推荐