条款27: 尽量少做转型动作
Minimize casting
C++规则的设计目标之一是,保证"类型错误"绝不可能发生.不幸的是,转型(cast)破坏了类型系统.
首先回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作.C风格的转型动作看起来像这样:
(T)expression; // 将expression转型为T
函数风格的转型动作看起来像这样:
T(expression); // 将expression转型为T
两种形式并无差别,纯粹只是小括号的摆放位置不同而已.
C++还提供了四种新式转型:
const_cast(expression);
dynamic_cast(expression);
reinterpret_cast(expression);
static_cast(expression);
各有不同的目的(详见static_cast, dynamic_cast, const_cast探讨):
const_cast 通常被用来
将对象的常量性转除(cast away the constness).它也是唯一有此能力的C++ style转型操作符.
dynamic_cast 主要用来
执行"安全向下转型"(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型.它是唯一无法由就是旧式语法执行的动作,也是唯一
可能耗费重大运行成本的转型动作.
reinterpret_cast 意图
执行低级转型,实际动作(及结果)可能取决于编译器,这就表示它
不可移植性.
static_cast 用来
强迫隐式转换(implicit conversions),例如将non-const 对象转为 const 对象(就像
条款3(尽量使用const)所为),或将 int 转为 double 等等.它也可以用来执行上述多种转换中的反向转换,例如将 void* 指针转换为typed指针,将pointer-to-base转为pointer-to-derived.但它无法将 const 转为 non-const——这只有 const_cast 才办得到.
旧式转换仍然合法,但
新式转换更受欢迎.原因是:第一,它们
很容易在代码中被辨识出来,因而得以简化"找出类型系统在哪个点被破坏"的过程.第二,
各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用.例如,如果打算将常量性(constness)去掉,除非使用新式转换中的 const_cast 否则无法通过编译.
许多程序员认为,
转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型.这是错误的观念.任何一种类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的代码.例如:
int x, y;
double d = static_cast(x)/y;
将 int x 转型为 double 几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int 的底层表述不同于 double 的底层表述.这或许不会令人惊讶,但请看下面这个例子:
class Base { ... };
class Derived : public Base { ... };
Derived d;
Base* pb = &d; // 隐式地将Derived* 转换为Base*
这里不过是建立一个base class 指针指向一个derived class 对象,但有时候上述的两个指针并不相同.这种情况下会有个偏移量(offset)在运行期间被施行于Derived* 指针上,用以取得正确的Base* 指针.
上述例子表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如"以Base*指向它"时的地址和"以Derived*指向它"时的地址,实际上一旦使用多重继承,这种事情几乎一直发生,因此避免做出"对象在C++如何布局"的假设)行为.
但请注意,只是有时候需要一个偏移量.
对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着"由于知道对象如何布局"而设计的转型,不一定在所有平台都行得通.
另一件关于转型的有趣事情是:很容易写出某些似是而非的代码.例如许多应用框架(application frameworks)都要求derived class 内的 virtual 函数代码的第一个动作就先调用base class 的对应函数.假设有个Window base class 和一个SpecialWindow derived class,两者都定义了 virtual 函数onResize.进一步假设SpecialWindow的OnResize函数被要求首先调用Window的OnResize.下面是实现方式之一,它看起来是对的,但实际是错的:
class Window {
public:
...
virtual void OnResize() { ... }
};
class SpecialWindow : public Window {
...
virtual void OnResize() { // derived onResize实现代码
static_cast(*this).OnResize(); // 将*this转型为Window,然后调用其OnResize
... // 这里进行SpecialWindow专属行为
}
};
这段程序将*this 转型为Window,对函数OnResize的调用也因此调用了Window::OnResize.但是,它调用的并不是当前对象上的函数,而是稍早转型动作建立的一个"*this对象的base class成分"的暂时副本身上的OnResize!再次强调上述代码并非在当期对象身上调用Window::OnResize之后又在该对象身上执行SpecialWindow专属动作.不,它是在"当前对象的base class成分"的副本上调用Window::OnResize,然后在当前对象身上执行SpecialWindow专属动作.如果Window::OnResize修改了对象内容,当前对象其实没有被改动,改动的是副本.
解决之道是拿掉转型动作,替换为真正想实施的动作.并不像哄骗编译器将*this 视为一个base class 对象,只是想调用base class 版本的OnResize函数,令它作用于当前对象身上.所以请这样写:
class SpecialWindow : public Window {
public:
virtual void OnResize() {
Window::OnResize(); // 调用Window::OnResize作用于*this身上
... // SpecialWindow的专属动作
}
};
在探究 dynamic_cast 设计意涵之前,值得注意的是,
dynamic_cast 的许多实现版本执行速度相当慢.例如至少有一个很普遍的实现版本基于"class名称的字符串比较",如果在四层深的单继承体系内的某个对象身上执行 dynamic_cast,则那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcpm调用,用以比较 class 名称.因此,除了对一般转型保持机敏与猜疑,更
应该在注重效率的代码中对dynamic_cast保持机敏与猜疑.
之所以需要dynamic_cast,通常是因为想在一个认定为derived class 对象上执行derived class 操作函数,但手上却只有一个"指向base"的pointer或reference,只能靠它们来处理对象.
有两个一般性做法可以避免这个问题.
第一,使用容器并在其中存储直接指向derived class 对象的指针(通常是智能指针,详见
条款13(以对象管理资源)),如此便消除了"通过base class接口处理对象"的需要.
当然了,这种做法使得无法在同一个容器内存储指针"指向所有可能的各种派生类"。如果需要处理多种类型,可能需要多个容器,它们都必须具备类型安全性.
另一种做法可让程序员通过base class 接口处理"所有的各种派生类",那就是在base class 内提供 virtual 函数做想做的对各种派生类做的事.
不论哪一种写法——"使用类型安全容器"或"将virtual函数往继承体系上方移动"——都并非完全正确,但在许多情况下它们都提供一个可行的dynamic_cast替代方案.当它们有效时,就应该欣然拥抱它们.
绝对必须避免的一件事就是所谓的"连串(cascading)dynamic_cast".
优秀的C++代码很少使用转型,但若要完全摆脱它们又太过不切实际.因此,应该尽可能隔离转型动作,通常把它隐藏在某个函数内,函数接口会保护调用者不受函数内部任何动作的影响.
注意:
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast.如果有个设计需要转型动作,试着发展无需转型的替代设计.
如果转型是必要的,试着将它隐藏于某个函数背后.客户随后可以调用该函数,而不需将转型放进他们自己的代码内.
宁可使用C++ style(新式)转型,不要使用旧式转型.前者很容易辨识出来,而且也比较有着分类的执掌.