More effective C++:隆重利用异通例格
副标题#e#
毫无疑问,异通例格是一个引人注目标特性。它使得代码更容易领略,因为它明晰地描写了一个函数可以抛出什么样的异常。可是它不可是一个有趣的注释。编译器在编译时有时可以或许检测到异通例格的纷歧致。并且假如一个函数抛出一个不在异通例格范畴里的异常,系统在运行时可以或许检测出这个错误,然后一个非凡函数unexpected将被自动地挪用。异通例格既可以做为一个指导性文档同时也是异常利用的强制约束机制,它仿佛有着很诱人的外表。
不外在凡是环境下,仙颜只是一层皮,外表的瑰丽并不代表其内涵的素质。函数unexpected缺省的行为是挪用函数terminate,而terminate缺省的行为是挪用函数abort,所以一个违反异通例格的措施其缺省的行为就是halt(遏制运行)。在激活的stack frame中的局部变量没有被释放,因为abort在封锁措施时不举办这样的排除操纵。对异通例格的得罪酿成了一场并不该该产生的劫难。
不幸的是,我们很容易就可以或许编写出导致产生这种劫难的函数。编译器仅仅部门地检测异常的利用是否与异通例格保持一致。一个函数挪用了另一个函数,而且后者大概抛出一个违反前者异通例格的异常,(A函数挪用B函数,因为B函数大概抛出一个不在A函数异通例格之内的异常,所以这个函数挪用就违反了A函数的异通例格 译者注)编译器差池此种环境举办检测,而且语言尺度也克制它们拒绝这种挪用方法(尽量可以显示告诫信息)。
譬喻函数f1没有声明异通例格,这样的函数就可以抛出任意种类的异常:
extern void f1(); // 可以抛出任意的异常
假设有一个函数f2通过它的异通例格来声明其只能抛出int范例的异常:
void f2() throw(int);
f2挪用f1长短常正当的,纵然f1大概抛出一个违反f2异通例格的异常:
#p#副标题#e#
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会抛出什么范例的异常。
#p#分页标题#e#
通过在措施在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[]抛出(拜见条款8)。假如你在函数里利用new操纵符(还拜见条款8),你必需为函数大概碰着bad_alloc异常作好筹备。
此刻常说防范胜于治疗(即做任何事都要未雨绸缪 译者注),可是有时却是防范坚苦而治疗容易。也就是说有时直接处理惩罚unexpected异常比防备它们被抛出要简朴。譬喻你正在编写一个软件,准确地利用了异通例格,可是你必需从没有利用异通例格的措施库中挪用函数,要防备抛出unexpected异常是不现实的,因为这需要改变措施库中的代码。
固然防备抛出unexpected异常是不现实的,可是C++答允你用其它差异的异常范例替换unexpected异常,你可以或许操作这个特性。譬喻你但愿所有的unexpected异常都被替换为UnexpectedException工具。你能这样编写代码:
class UnexpectedException {}; // 所有的unexpected异常工具被
//替换为这种范例工具
void convertUnexpected() // 假如一个unexpected异常被
{
// 抛出,这个函数被挪用
throw UnexpectedException();
}
通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。:
set_unexpected(convertUnexpected);
当你这么做了今后,一个unexpected异常将触发挪用convertUnexpected函数。Unexpected异常被一种UnexpectedException新异常范例替换。假如被违反的异通例格包括UnexpectedException异常,那么异常通报将继承下去,仿佛异通例格老是获得满意。(假如异通例格没有包括UnexpectedException,terminate将被挪用,就仿佛你没有替换unexpected一样)
另一种把unexpected异常转酿成知名范例的要领是替换unexpected函数,让其从头抛出当前异常,这样异常将被替换为bad_exception。你可以这样编写:
void convertUnexpected() // 假如一个unexpected异常被
{
//抛出,这个函数被挪用
throw; // 它只是从头抛出当前
} // 异常
set_unexpected(convertUnexpected);
// 安装 convertUnexpected
// 做为unexpected
// 的替代品
假如这么做,你应该在所有的异通例格里包括bad_exception(或它的基类,尺度类exception)。你将不必再担忧假如碰着unexpected异常会导致措施运行终止。任何不听话的异常都将被替换为bad_exception,这个异常取代本来的异常继承通报。
到此刻你应该领略异通例格能导致大量的贫苦。编译器仅仅能部门地检测它们的利用是否一致,在模板中利用它们会有问题,一不留意它们就很容易被违反,而且在缺省的环境下它们被违反时会导致措施终止运行。异通例格尚有一个缺点就是它们能导致unexpected被触发纵然一个high-level挪用者筹备处理惩罚被抛出的异常,好比下面这个险些一字不差地来自从条款11例子:
class Session { // for modeling online
public: // sessions
~Session();
...
private:
static void logDestruction(Session *objAddr) throw();
};
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) { }
}
session的析构函数挪用logDestruction记录有关session工具被释放的信息,它明晰地要捕捉从logDestruction抛出的所有异常。可是logDestruction的异通例格暗示其不抛出任何异常。此刻假设被logDestruction挪用的函数抛出了一个异常,而logDestruction没有捕捉。我们不会期望产生这样的工作,每每正如我们所见,很容易就会写出违反异通例格的代码。当这个异常通过logDestruction通报出来,unexpected将被挪用,缺省环境下将导致措施终止执行。这是一个正确的行为,这是session析构函数的作者所但愿的行为么?作者想处理惩罚所有大概的异常,所以仿佛不该该不给session析构函数里的catch块执行的时机就终止措施。假如logDestruction没有异通例格,这种工作就不会产生。(一种防备的要领是如上所描写的那样替换unexpected)
#p#分页标题#e#
以全面的角度去对待异通例格长短常重要的。它们提供了优秀的文档来说明一个函数抛出异常的种类,而且在违反它的环境下,会有可骇的功效,措施被当即终止,在缺省时它们会这么做。同时编译器只会部门地检测它们的一致性,所以他们很容易被不经意地违反。并且他们会阻止high-level异常处理惩罚器来处理惩罚unexpected异常,纵然这些异常处理惩罚器知道如何去做。综上所述,异通例格是一个应被隆重利用的公族。在把它们插手到你的函数之前,应思量它们所带来的行为是否就是你所但愿的行为。