条款 5:对定制的“类型转换函数”保持警觉
C++允许编译器在不同类型之间执行隐式转换(implicit conversions)。继承了C 的伟大传统,这个语言允许默默地将char转换为 int,将 short 转换为 double。这便是为什么你可以将一个 short 交给一个“期望获得 double”的函数而仍能成功的原因。C++还存在更令人害怕的转型(我指的是可能遗失信息的那种),包括将 int转换为 short,以及将 double(或其他东西)转换为 char。
你对这类转型无能为力,因为它们是语言提供的。然而当你自己的类型登场,你便有了更多的控制能力,因为你可以选择是否提供某些函数,供编译器拿来作为隐式类型转换之用。
两种函数允许编译器执行这样的转换:单自变量 constructors 和隐式类型转换操作符。所谓单自变量 constructors 是指能够以单一自变量成功调用的constructors。如此的 constructor 可能声明拥有单一参数,也可能声明拥有多个参数,并且除了第一参数之外都有默认值。下面是两个例子:
所谓隐式类型转换操作符,是一个拥有奇怪名称的 member function:关键词operator 之后加上一个类型名称。你不能为此函数指定返回值类型,因为其返回值类型基本上已经表现于函数名称上。例如,为了让 Rational objects 能够被隐式转换为 doubles(这对掺杂有 Rational objects的混合型算术运算可能有用),你可能定义 class Rational 如下:
或许这一切对你而言都只是复习。那很好,因为我真正要解释的是,为什么最好不要提供任何类型转换函数。
根本问题在于,在你从未打算也未预期的情况下,此类函数可能会被调用,而其结果可能是不正确、不直观的程序行为,很难调试。
让我们先处理隐式类型转换操作符,因为它比较容易掌握。假设你有一个 class用来表现分数(rational numbers)。你希望像内建类型一样地输出 Rational objects内容。也就是说你希望能够这么做:
更进一步假设你忘了为 Rational 写一个 operator<<,那么你或许会认为上述打印动作不会成功,因为没有适当的 operator<< 可以调用。但是你错了。你的编译器面对上述动作,发现不存在任何 operator<< 可以接受一个 Rational,但它会想尽各种办法(包括找出一系列可接受的隐式类型转换)让函数调用动作成功。“可被接受的转换程序”定义十分复杂,但本例中你的编译器发现,只要调用Rational::operator double,将 r 隐式转换为 double,调用动作便能成功。于是上述代码将 r 以浮点数而非分数的形式输出。这虽然不至于造成灾难,却显示了隐式类型转换操作符的缺点:它们的出现可能导致错误(非预期)的函数被调用。
解决办法就是以功能对等的另一个函数取代类型转换操作符。为了允许将Rational 转换为 double,不妨以一个名为 asDouble 的函数取代 operator double:
如此的 member function 必须被明确调用:
大部分时候,“必须明白调用类型转换函数”虽然带来些许不便,却可因为“不再默默调用那些其实并不打算调用的函数”而获得弥补。一般而言,愈有经验的 C++程序员愈可能避免使用类型转换操作符。C++标准委员会中隶属标准程序库(见条款 E49和条款 35)小组的那些成员,应该算是最有经验的 C++程序员了吧,这或许便是为什么标准程序库的 string 类型并未含有“从 string object 至 C-style char* 的隐式转换函数”的原因。他们提供的办法是用一个显式的 member function c_str来执行上述转换行为。巧合吗?我想不是。
通过单自变量 constructors 完成的隐式转换,较难消除。此外,这些函数造成的问题在许多方面比隐式类型转换操作符的情况更不好对付。
举个例子,考虑一个针对数组结构而写的 class template。这些数组允许用户指定索引值的上限和下限:
上述 class 的第一个 constructor 允许 clients 指定某个范围的数组索引,例如10~20。身为一个双自变量 constructor,此函数没有资格成为类型转换函数。第二个 constructor 允许用户只指定数组的元素个数,便可定义出 Array objects(这很类似内建数组)。它可以被用来作为一个类型转换函数,结果导致无尽苦恼。
例如,考虑一个用来对 Array<int> 对象进行比较动作的函数,以及一小段代码:
我试图将 a 的每一个元素拿来和 b 的对应元素比较,但是当我键入 a 时却意外地遗漏了下标(方括号)语法。我当然希望我的编译器发挥挑错功能,将它挑出来,但它却一声也不吭。因为它看到的是一个 operator==函数调用,夹带着类型为 Array<int> 的自变量 a 和类型为 int 的自变量 b[i],虽然没有这样的operator==函数可被调用,我的编译器却注意到,只要调用 Array<int>constructor(需一个 int 作为自变量),它就可以将 int 转为 Array<int> object。于是它便放手去做,因而产生类似这样的代码:
于是,循环的每一次迭代都拿 a 的内容来和一个大小为 b[i] 的临时数组(其内容想必未定义)做比较。此种行为不仅不令人满意,而且非常没有效率。因为每次走过这个循环,我们都必须产生和销毁一个临时的 Array<int> object(见条款 19)。
只要不声明隐式类型转换操作符,便可将它所带来的害处避免。但是单自变量constructors 却不那么容易去除,毕竟你可能真的需要提供一个单自变量constructors 给你的客户使用。与此同时,你也可能希望阻止编译器不分青红皂白地调用这样的 constructors。幸运的是有一种(事实上是两种)做法可以两者兼顾:一个是简易法,另一个可在你的编译器不支持简易法的情况下使用。
简易法是最新使用的 C++特性:关键词 explicit。这个特性之所以被导入,就是为了解决隐式类型转换带来的问题。其用法十分直接易懂,只要将 constructors声明为 explicit,编译器便不能因隐式类型转换的需要而调用它们。不过显式类型转换仍是允许的:
上述使用 static_cast(见条款 2)的那一行中,两个“>”字符之间的空格是有必要的。如果上述那行写成这样:
它就有了不同的意义,因为 C++编译器将“>>”视为单一的词元(token)。所以如果没有在“>”字符之间加上一个(一些)空格,上行语句会发生语法错误。
如果你的编译器尚不支持关键词 explicit,你就只得走回头路,通过以下做法阻止单自变量 constructors 成为隐式类型转换函数。
稍早我曾说过,关于“哪些隐式类型转换程序是合法的,哪些不是”,其间有着复杂的游戏规则。其中一条规则是:没有任何一个转换程序(sequence of conversions)可以内含一个以上的“用户定制转换行为(亦即单自变量 constructor 或隐式类型转换操作符)”。为了适当架构起你的 classes,你可以利用这项规则,让你希望拥有的“对象构造行为”合法化,并让你不希望允许的隐式构造非法化。
让我们再次考虑 Array template。你需要一种方法,不但允许以一个整数作为constructor 自变量来指定数组大小,又能阻止一个整数被隐式转换为一个临时性Array 对象。于是你首先产生一个新的 class,名为 ArraySize。此型对象只有一个目的:用来表现即将被产生的数组的大小。然后你修改 Array 的单自变量
constructor,让它接收一个 ArraySize 对象,而非一个 int。代码如下:
在这里,把 ArraySize 嵌套放进 Array 内,强调一个事实:它永远与 Array搭配使用。我们也把 ArraySize 放在 Array 的 public 区,使任何人都能够使用它。好极了!
现在考虑当我们通过 Array 的“单自变量 constructor”定义一个对象时,会发生什么事:
你的编译器被要求调用 Array<int> class 中的一个自变量为 int 的constructor,但其实并不存在这样的 constructor。编译器知道它能够将 int 自变量转换为一个临时的 ArraySize 对象,而该对象正是 Array<int> constructor 需要的,所以编译器便依其所好执行了这样的转换。于是函数调用(以及随附的对象构造行为)得以成功。
“以一个 int 自变量构造起一个 Array 对象”这个事实依然可靠有效,但是除非“你希望避免的类型转换动作”确实被阻止,否则那也算不上是什么好消息。是的,它们的确是被阻止了。再次考虑这段代码:
编译器需要一个类型为 Array<int> 的对象在“==”的右手边,得以针对Array<int> 对象调用 operator==,但是此刻并没有“单一自变量,类型为 int”这样的 constructor。此外,编译器不能考虑将 int 转换为一个临时性的 ArraySize对象,然后再根据这个临时对象产生必要的 Array<int> 对象,因为那将调用两个用户定制转换行为,一个将 int 转换为 ArraySize,另一个将 ArraySize 转换为 Array<int>。如此的转换程序是禁止的,所以编译器对以上代码发出错误消息。
本例对 ArraySize class 的运用,或许看起来像是特殊安排的情况,但它其实是更一般化技术的一个特别实例。类似 ArraySize 这样的 classes,往往被称为proxy classes,因为它的每一个对象都是为了其他对象而存在的,好像其他对象的代理人(proxy)一般。ArraySize 对象只是“用来指定 Array 大小”的整数替身而已。Proxy objects 让你得以超越外观形式(本例为隐式类型转换),进而控制你的软件行为,是很值得学习的一项技术。如何学习?条款 30便是一个开始。
然而在你进入 proxy classes 主题之前,请再温习一下本条款的功课。是的,允许编译器执行隐式类型转换,害处将多过好处。所以不要提供转换函数,除非你确定你需要它们。