在异常捕获加入C++几年后,标准化委员会加入了一个叫做异常规范的补充特性。本文将介绍异常规范并质疑其可用性。
问题
考虑下面的函数原型:
void validate(int code);
通常,第三方的库把相似的声明分类放在头文件里面,把实现对用户隐藏起来。用户如何知道这个函数是否抛出一个异常和在什么情况下抛出异常呢?显然,这种声明没有提供任何线索。Validate()可能是一个抛出异常的函数,甚至可能是一个完全不知道C++异常的C函数抛出了异常。
异常在1989年加入了C++。几年的困扰使得标准化委员会加入了异常规范。
异常规范基础
一个异常规范描述了一个函数允许抛出哪些异常。没有列在异常规范里的异常将不会从函数里抛出。一个异常规范包括在函数的参数列表后面添加的关键字throw。在throw后面是一个异常列表。
void validate(int code) throw(bad_code, no_auth);
异常规范并不是函数原型的一部分。因此,它不影响重载函数。那就是说,函数指针和成员函数可以包括一个异常规范;
void (*pf)(int) throw(string, int);
pf是指向一个可能抛出string类型或int类型异常的函数的指针。你可以让其指向一个与其有同样异常规范限制的函数或者一个异常规范限制更严格的函数。如果异常规范A的异常集合是异常规范B的异常集合的子集,称A比B的限制更严格。换句话说,A包含的每个异常都在B中,但是反过来却不是这样的。
//more restrictive than pf:
void e(int) throw (string);
//as restrictive as pf:
void f(int) throw(string, int);
//less restrictive than pf:
void g(int) throw(string, int, bool);
pf = e; //fine
pf = f; //fine
pf = g; //error
异常规范和继承
一个重载虚函数不能扩展在基类的函数里声明的异常集,但是可以缩小它。
让我们看一个实际的例子。假如你有下面的类层次结构以及相关的异常类集合:
class clock_fault{/*...*/};
class Exception{/*...*/}; //base for other exceptions
class hardware_fault: public Exception{/*...*/};
class logical_error: public Exception{/*...*/};
class invalid_protocol: public Exception{/*...*/};
class RemovableDevice
{
public:
virtual int connect(int port) throw(hardware_fault, logical_error);
connect(int port) throw(hardware_fault, logical_error);
virtual int transmit(char* buff) throw(invalid_protocol);
};
class Scanner:public RemovableDevice
{
public:
int connect(int port) throw(hardware_fault); //OK
int transmit(char* buff);
throw(invalid_protocol, clock_fault); //Error
};
RemovableDevice::connect()的异常规范允许它抛出hardware_fault和logical_error异常(以及任何派生自这些类的异常)。重载函数Scanner::connect()缩小了这个规范。从这个函数抛出任何除hardware_fault以外的异常,包括logical_error,都不允许。
Scanner::transmit()的异常规范的形式是错误的。它包括一个没有在RemovableDevice::transmit的异常规范里出现的异常:clock_fault。如果你尝试编译这段代码,编译器将提示异常规范冲突。
空的异常规范和遗漏的异常规范
一个没有异常规范的函数允许所有的异常。一个异常规范为空的函数不允许任何异常:
class File
{
public:
int open(FILE* ptr); //may throw any exception
int close(FILE* ptr) throw(); //doesn't throw
};
当你声明一个空的异常规范,你需要总是检查是否有破坏它的风险。
异常规范的实现
异常规范在运行时实现。当一个函数破坏了它的异常规范,std::unexpected()将被调用。unexpected()调用一个之前通过std::set_unexpected()注册的用户定义函数。如果没有通过set_unexpected()注册函数,unexpected()调用std::terminate()来无条件终止程序运行。
异常规范——理论与实践
异常规范似乎是备受赞扬的东西。它们不只明确的在文档上提出了一种函数的异常策略,而且C++也实现了它们。
在开始,C++社区强烈的欢迎它们。许多指南和手册的作者开始在所有的地方使用它们。课本里的一个典型的类就像这样:
class Foo
{
public:
Foo() throw();
~Foo() throw();
Foo(const Foo&) throw();
Foo& operator = (const Foo&) throw();
// ... etc., etc.
};
没过多久,程序员就意识到异常规范是相当麻烦的。异常是动态的。不可能总是预见的到在运行时将抛出哪些异常。如果一个有异常规范的函数g()调用一个有更小限制或者没有限制的异常规范的函数f(),将会发生什么呢?
void f();
void g() throw(X)
{
f(); //OK, but problematic
}
如果f()抛出一个不是X的异常,g()可能会破坏它的异常规范。
性能是另外一个问题。异常总会产生性能代价。执行异常规范还会产生额外的代价,以为实现是在运行时执行的。
因为这些原因以及其他一些原因,异常规范迅速失去了它们的光彩。今天,你很难再在新的代码或者课本(讽刺的是,第一批采用它们的课本也是第一批悄悄丢弃它们的)里发现它们。
结论
异常规范是那些理论上似乎可行但是在现实世界被证明的声明狼藉的特性之一。你可能会问我为什么花时间来讨论它们。有两点原因:
第一,传统的使用异常规范的代码依然存在。读这样的代码——更重要的是正确的使用它们,要求熟悉这一属性。
第二,异常规范在程序语言设计上上了一课。很多程序语言采用C++的异常捕获模型,包括异常规范。当C++社区意识到异常规范没有那么好时,那些语言已经沉迷于这一特性的好处。
今天,有一种要求对C++加入finally的压力。这种架构在没有析构函数的Java里面很有用。尽管如此,在C++语言里,finally是多余的,因为在一件异常事件里,你可以通过在析构函数里实现无条件的清理操作。因此为什么要提议finally呢?很简单,因为Java的程序员仍然按照Java的思维方式编写C++代码。异常规范告诉我们务必必须对添加一个没有被彻底测试的特性非常小心。
翻译自:http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=109