条款 2:最好使用 C++转型操作符
想想低阶转型动作。它几乎像 goto一样被视为程序设计上的“贱民”。尽管如此,它却仍能够苟延残喘,因为当某种情况愈来愈糟,转型可能是必要的。是的,当某种情况愈来愈糟,转型是必要的!
不过,旧式的 C转型方式并非是唯一选择。它几乎允许你将任何类型转换为任何其他类型,这是十分拙劣的。如果每次转型都能够更精确地指明意图,则更好。举个例子,将一个 pointer-to-const-object 转型为一个 pointer-to-non-const-object (也就是说只改变对象的常量性),和将一个 pointer-to-base-class-object 转型为一个 pointer-to-derived-class-object(也就是完全改变了一个对象的类型),其间有很大的差异。传统的 C 转型动作对此并无区分(这应该不会造成你的惊讶,因为 C 式转型是为 C 设计的,不是为了 C++)。
旧式转型的第二个问题是它们难以辨识。旧式转型的语法结构是由一对小括号加上一个对象名称(标识符)组成,而小括号和对象名称在 C++的任何地方都有可能被使用。因此,我们简直无法回答最基本的转型相关问题“这个程序中有使用任何转型动作吗?”。因为人们很可能对转型动作视而不见,而诸如 grep 之类的工具又无法区分语法上极类似的一些非转型写法。
为解决 C 旧式转型的缺点,C++导入 4个新的转型操作符(cast operators):static_cast,const_cast,dynamic_cast 和 reinterpret_cast。对大部分使用目的而言,面对这些操作符你唯一需要知道的便是,过去习惯的写码形式:
现在应该改为这样:
举个例子,假设你想要将一个 int 转型为一个 double,以强迫一个整数表达式导出一个浮点数值来。采用 C 旧式转型,可以这么做:
如果采用新的 C++转型法,应该这么写:
这种形式十分容易被辨识出来,不论是对人类或是对工具程序而言。
static_cast 基本上拥有与 C 旧式转型相同的威力与意义,以及相同的限制。例如,你不能够利用 static_cast 将一个 struct 转型为 int,或将一个 double转型为 pointer;这些都是 C 旧式转型动作原本就不可以完成的任务。static_cast甚至不能够移除表达式的常量性(constness),因为有一个新式转型操作符const_cast 专司此职。
其他新式 C++转型操作符适用于更集中(范围更狭窄)的目的。const_cast 用来改变表达式中的常量性(constness)或变易性(volatileness)。使用 const_cast,便是对人类(以及编译器)强调,通过这个转型操作符,你唯一打算改变的是某物的常量性或变易性。这项意愿将由编译器贯彻执行。如果你将 const_cast 应用于上述以外的用途,那么转型动作会被拒绝。下面是个例子:
显然,const_cast 最常见的用途就是将某个对象的常量性去除掉。
第二个特殊化的转型操作符是 dynamic_cast,用来执行继承体系中“安全的向下转型或跨系转型动作”。也就是说你可以利用 dynamic_cast,将“指向 base class objects的 pointers或 references”转型为“指向 derived(或 sibling base)class objects的 pointers 或 references”,并得知转型是否成功1。如果转型失败,会以一个 null指针(当转型对象是指针)或一个 exception(当转型对象是 reference)表现出来:
dynamic_cast 只能用来协助你巡航于继承体系之中。它无法应用在缺乏虚函数(请看条款 24)的类型身上,也不能改变类型的常量性(constness):
如果你想为一个不涉及继承机制的类型执行转型动作,可使用 static_cast;要改变常量性(constness),则必须使用 const_cast。
最后一个转型操作符是 reinterpret_cast。这个操作符的转换结果几乎总是与编译平台息息相关。所以 reinterpret_casts 不具移植性。
reinterpret_cast 的最常用用途是转换“函数指针”类型。假设有一个数组,存储的都是函数指针,有特定的类型:
如果没有转型,不可能办到这一点,因为 doSomething 的类型与funcPtrArray 所能接受的不同。funcPtrArray 内各函数指针所指函数的返回值是 void,但 doSomething 的返回值却是 int:
函数指针的转型动作,并不具移植性(C++不保证所有的函数指针都能以此方式重新呈现),某些情况下这样的转型可能会导致不正确的结果(见条款 31),所以你应该尽量避免将函数指针转型,除非你已走投无路,像是被逼到墙角,而且有一把刀子抵住你的喉咙。一把锐利的刀子,非常锐利的刀子。
如果你的编译器尚未支持这些新式转型动作,你可以使用传统转型方式来取代static_cast,const_cast 和 reinterpret_cast。甚至可以利用宏(macros)来仿真这些新语法。
上述新语法的使用方式如下:
这些近似法当然不像其本尊那么安全,但如果你现在就使用它们,一旦你的编译器开始支持新式转型,程序升级的过程便可简化。
至于 dynamic_cast,没有什么简单方法可以模拟其行为,不过许多程序库提供了一些函数,用来执行继承体系下的安全转型动作。如果你手上没有这些函数,而却必须执行这类转型,你也可以回头使用旧式的 C 转型语法,但它们不可能告诉你转型是否成功。当然,你也可以定义一个宏,看起来像 dynamic_cast,就像你为其他转型操作符所做的那样:
这个近似法并非执行真正的 dynamic_cast,所以它无法告诉你转型是否成功。
我知道,我知道,这些新式转型操作符看起来又臭又长。如果你实在看它们不顺眼,值得安慰的是 C 旧式转型语法仍然可继续使用。然而这么一来也就丧失了新式转型操作符所提供的严谨意义与易辨识度。如果你在程序中使用新式转型法,比较容易被解析(不论是对人类还是对工具而言),编译器也因此得以诊断转型错误(那是旧式转型法侦测不到的)。这些都是促使我们舍弃 C 旧式转型语法的重要因素。至于可能的第三个因素是:让转型动作既丑陋又不易键入(typing),或许未尝不是件好事。