Gotcha #64: Throwing String Literals
许多C++编程教本的作者在展示异常机制时都抛出字符文字串(character string literals)信息:
throw "Stack underflow!";
他们知道这种实作手法本应回避,但是他们还是这样做了,因为那只是“教学性示例”。不幸的是,这样做示例就隐含了“模仿这个示例”的建议(译注:毕竟,读者当然会倾向于效仿所看教本中的示例来学习),而这些作者们通常忽视了还要提示读者:“真正照这样做的话,会招致刻意伤害和厄运”。
绝不要抛出string literals作为exception objects(异常对象)。从原理上讲,原因是:这些exception objects最终应该被捕获,而且应该是根据其型别(type)来捕获,而不是根据其值(value)来捕获:
try {
// . . .
}
catch( const char *msg ) {
string m( msg );
if( m == "stack underflow" ) // . . .
else if( m == "connection timeout" ) // . . .
else if( m == "security violation" ) // . . .
else throw;
}
抛出和捕获string literals所产生的实际影响是,经由exception object的型别,几乎没有包含任何有关异常的信息。这种不甚严密的做法要求“catch clause拦截每一个同类异常并通过检视其值(value)来判断是否合乎捕获条件”。更糟的是,对值的比较也是很不严密的;一旦该“错误消息”在大小写或格式方面有改变,这种比较就毫无效用了。如果在上面的例子中发生这种情况,我们就永远无法发觉“栈发生下溢(stack underflow)”的情况。
同样的情况也存在于其它预定义型别和标准型别的异常中。抛出integers,floating point numbers,strings,或者(在一个糟糕透顶的日子)float vector组成的sets,都会引发类似的问题。简单地说,“抛出预定义型别之异常对象”的问题在于:当我们捕获到一个此种异常时,我们无法知道它代表什么意思,从而也无法确定如何处理异常。抛出该异常的人好像在愚弄我们:“发生了非常非常糟糕的事情!你猜是什么?”而我们别无选择,只好玩起做作的猜谜游戏,并很可能会玩输。
所谓exception type(异常型别)是一个用来代表异常的抽象数据型别(abstract data type)。设计这些异常型别时所要遵循的原则无异于设计其它任何抽象数据型别:辨识并具名一个概念,为其定出抽象的操作集合并实现之。在实现过程中,要考虑初始化、拷贝以及型别转换等问题。很简单嘛。用string literal来表示一个异常,较之用complex number来表示一个异常,同样都是没有多大意义。从理论上讲这样做或许会凑效,但从实际上讲这会变得冗长乏味,臭虫百出。
当我们抛出“代表stack underflow()的异常”时,我们试图表达什么样的抽象概念呢?噢。可不就是它吗。
class StackUnderflow {};
通常,一个exception object的型别要能传达所有关于该异常的信息;而对于exception types来说,“经由显式声明的成员函数来进行信息发放(dispense)”也没什么不寻常的。然而,“提供描述性文本的能力”经常是信手拈来的。其它不太常需要的相关信息也可以记录到exception object当中:
class StackUnderflow {
public:
StackUnderflow( const char *msg = "stack underflow" );
virtual ~StackUnderflow();
virtual const char *what() const;
// . . .
};
如果提供了能返回描述性文本的函数,那么其应该是一个名为what的virtual member function,具有上述的表达形式。这样做是考虑到该异常与标准异常型别的正交性,因为标准异常型别都提供了这个函数。事实上,让自定义的异常型别派生自标准异常型别,通常是个好主意:
class StackUnderflow : public std::runtime_error {
public:
explicit StackUnderflow( const char *msg = "stack underflow" )
: std::runtime_error( msg ) {}
};
这就使我们可以将其作为StackUnderflow、较为一般化的runtime_error,或更具一般性的标准异常(runtime_error的public base class)来捕获。提供一个更一般化但非标准的exception type,通常也是个好主意。一般来说,这种型别被作为base class使用,特定模块或程序库可能抛出的所有exception types都派生自它:
class ContainerFault {
public:
virtual ~ContainerFault();
virtual const char *what() const = 0;
// . . .
};
class StackUnderflow
: public std::runtime_error, public ContainerFault {
public:
explicit StackUnderflow( const char *msg = "stack underflow" )
: std::runtime_error( msg ) {}
const char *what() const
{ return std::runtime_error::what(); }
};
最后要说的是,为exception types提供寻常的copy和destruction语义也是必需的。特别是,抛出一个异常就暗示着“copy construct该exception type对象必须是合法的”,因为这正是运行期异常机制在异常被抛出时要做的事情(详见Gotcha条款65),而当该异常被处理之后也必须销毁这个拷贝。通常可以让编译器为我们自动编写这些操作(详见Gotcha条款49):
class StackUnderflow
: public std::runtime_error, public ContainerFault {
public:
explicit StackUnderflow( const char *msg = "stack underflow" )
: std::runtime_error( msg ) {}
// StackUnderflow( const StackUnderflow & );
// StackUnderflow &operator =( const StackUnderflow & );
const char *what() const
{ return std::runtime_error::what(); }
};
现在,stack type的用户可以自行选择,让stack underflow作为不同层级的异常型别被捕获,其可以是StackUnderflow(用户知道自己在使用stack type,希望特别关注它)、较一般化的ContainerFault(用户知道自己在使用container library,希望捕获任何container 引发的错误)、runtime_error(用户不知道自己是否在使用哪个container library,希望处理任何标准运行期错误),或最一般化的exception(用户准备处理任何标准异常)。