在异常捕获加入
C++
几年后,标准化委员会加入了一个叫做异常规范的的补充特性。本文将介绍异常规范并质疑其可用性。
问题
考虑下面的函数原型:
通常,第三方的库把相似的声明分类放在头文件里面,把实现对用户隐藏起来。用户如何知道这个函数是否抛出一个异常和在什么情况下抛出异常呢?显然,这种声明并没有提供任何线索。
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);
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.asp?g=cplusplus&seqNum=109&rl=1
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1452376