Item 27: 最少化 casting(强制转型)
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
C++ 的规则被设计为保证不会发生类型错误。在理论上,如果你的程序想顺利地通过编译,它就不应该试图针对任何 objects 做任何不安全的或无意义的操作。这是一个非常有价值的保证,你不应该轻易地放弃它。
不幸的是,casts(强制转型)搅乱了 type system(类型系统)。它会导致各种各样的麻烦,其中一些容易被察觉,另一些则格外地微妙。如果你从 C,Java,或 C# 转到 C++,请一定注意,因为 casting(强制转型)在那些语言中比在 C++ 中更有必要,危险也更少。但是 C++ 不是 C,也不是 Java,也不是 C#。在这一语言中,casting(强制转型)是一个需要你带有极大的敬畏之心才可以靠近的特性。
我们就从回顾 casting(强制转型)语法开始,因为对于同样的 cast(强制转型)通常有三种不同的写法。C-style casts(C 风格强制转型)如下:
(T) expression // cast expression to be of type T
Function-style casts(函数风格强制转型)使用这样的语法:
T(expression) // cast expression to be of type T
这两种形式之间没有含义上的不同,它纯粹就是一个把括号放在哪的问题。我把这两种形式称为 old-style casts(旧风格强制转型)。
C++ 同时提供了四种新的 cast(强制转型)形式(通常称为 new-style(新风格)或 C++-style casts(C++ 风格强制转型)):
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
每一种适用于特定的目的:
- const_cast 一般用于强制消除 objects 的 constness(常量性)。它是唯一能做到这一点的 C++-style cast(C++ 风格强制转型)。
- dynamic_cast 主要用于执行 “safe downcasting”(“安全的向下转型”),也就是说,用于确定一个 object 是否是一个 inheritance hierarchy(继承体系)中的一个特定类型。它是唯一不能用 old-style(旧风格)语法执行的 cast(强制转型)。也是唯一可能有很大运行时成本的 cast(强制转型)。(过一会儿我会提供与此相关的细节。)
- reinterpret_cast 是特意用于底层的导致 implementation-dependent(实现依赖)(也就是说,不可移植)的结果的 casts(强制转型),例如,将一个 pointer 转型为一个 int。这样的 casts(强制转型)在底层代码以外应该极为罕见。在本书中我只用了一次,而且还仅仅是在讨论你应该如何为 raw memory(裸内存)写一个 debugging allocator(可调试分配器)的时候(参见 Item 50)。
- static_cast 可以被用于 force implicit conversions(强制隐式转换)(例如,non-const object 到 const object(就像 Item 3 中的),int 到 double,等等)。它还可以用于执行多数这样的转换的反向转换(例如,void* 指针到有类型指针,pointer-to-base(基类指针)到 pointer-to-derived(派生类指针)),虽然它不能将 const objects 转型为 non-const objects。(只有 const_cast 能做到这一点。)
old-style casts(旧风格强制转型)依然是合法的,但是新的形式更可取。首先,在代码中它们更容易识别(对人和对像 grep 这样的工具都是如此),这样就简化了在代码中寻找 type system(类型系统)被搅乱的地方的过程。第二,更精确地指定每一个 cast(强制转型)的目的,使得编译器诊断使用错误成为可能。例如,如果你试图使用一个 const_cast 以外的 new-style cast(新风格强制转型)来消除 constness(常量性),你的代码将无法编译。
当我想要调用一个 explicit constructor(显式构造函数)用来传递一个 object 给一个函数的时候,大概就是我仅有的使用 old-style cast(旧风格强制转型)的时候。例如:
class Widget {
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15)); // create Widget from int
// with function-style cast
doSomeWork(static_cast<Widget>(15)); // create Widget from int
// with C++-style cast
由于某种原因,有条不紊的 object creation(对象创建)感觉上不像一个 cast(强制转型),所以在此情况下我或许会用 function-style cast(函数风格强制转型)取代 static_cast。还有,在你写出那些导致 core dump 的代码时,你通常都感觉你有很完美的理由,所以你最好忽略你的感觉并始终都使用 new-style casts(新风格强制转型)。
很多程序员认为 casts(强制转型)除了告诉编译器将一种类型看作另一种之外什么都没做,但这是错误的。任何种类的类型转换(无论是通过 casts(强制转型)的 explicit(显式)的还是编译器添加的 implicit(隐式)的)常常导致运行时的可执行代码。例如,在这个代码片断中,
int x, y;
...
double d = static_cast<double>(x)/y; // divide x by y, but use
// floating point division
int x 到一个 double 的 cast(强制转型)理所当然要生成代码,因为在大多数系统架构中,一个 int 的底层表示与一个 double 的不同。这或许还不怎么令人惊讶,但是下面这个例子可能会让你稍微开一下眼:
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d; // implicitly convert Derived* → Base*
这里我们只是创建了一个指向一个 derived class object(派生类对象)的 base class pointer(基类指针),但是有时候,这两个指针的值并不相同。在这种情况下,会在运行时在 Derived* 指针上应用一个偏移量以得到正确的 Base* 指针值。
这后一个例子表明一个单一的 object(例如,一个 Derived 类型的 object)可能会有不止一个地址(例如,它的被一个 Base* 指针指向的地址和它的被一个 Derived* 指针指向的地址)。这在 C 中就不会发生,也不会在 Java 中发生,也不会在 C# 中发生,它仅在 C++ 中发生。实际上,如果使用了 multiple inheritance(多继承),则一定会发生,但是在 single inheritance(单继承)下也会发生。与其它事情合在一起,就意味着你通常应该避免对 C++ 如何摆放东西做出假设,你当然也不应该基于这样的假设执行 casts(强制转型)。例如,将一个 object 的地址强制转型为 char* 指针,然后对其使用指针运算,这几乎总是会导致 undefined behavior(未定义行为)。
但是请注意我说一个偏移量是“有时”被需要。objects 被摆放的方法和他们的地址的被计算的方法在不同的编译器之间有所变化。这就意味着仅仅因为你的“我知道东西是如何摆放”的 casts(强制转型)能工作在一个平台上,并不意味着它们也能在其它平台工作。这个世界被通过痛苦的道路学得这条经验的可怜的程序员所充满。
关于 casts(强制转型)的一件有趣的事是很容易写出看起来对(在其它语言中也许是对的)实际上错的东西。例如,许多 application framework(应用程序框架)要求 derived classes(派生类)中 virtual member function(虚拟成员函数)的实现要首先调用它们的 base class(基类)的对应物。假设我们有一个 Window base class(基类)和一个 SpecialWindow derived class(派生类),它们都定义了 virtual function(虚拟函数)onResize。进一步假设 SpecialWindow 的 onResize 被期望首先调用 Window 的 onResize。这就是实现这个的一种方法,它看起来正确实际并不正确:
class Window { // base class
public:
virtual void onResize() { ... } // base onResize impl
...
};
class SpecialWindow: public Window { // derived class
public:
virtual void onResize() { // derived onResize impl;
static_cast<Window>(*this).onResize(); // cast *this to Window,
// then call its onResize;
// this doesn't work!
... // do SpecialWindow-
} // specific stuff
...
};
我突出了代码中的 cast(强制转型)。(这是一个 new-style cast(新风格强制转型),但是使用一个 old-style cast(旧风格强制转型)也于事无补。)正像你所期望的,代码将 *this 强制转型为一个 Window。因此调用 onResize 的结果就是调用 Window::onResize。你可能并不期待它没有在 current object(当前对象)上调用那个函数!作为替代,cast(强制转型)创建了一个新的,临时的 *this 的 base class part(基类部分)的 copy(拷贝),然后在这个拷贝上调用 onResize!上面的代码没有在 current object(当前对象)上调用 Window::onResize,然后再在这个 object 上执行 SpecialWindow 特有的动作——它在在 current object(当前对象)上执行 SpecialWindow 特有的动作之前,在一份 current object(当前对象)的 copy of the base class part(基类部分的拷贝)上调用了 Window::onResize。如果 Window::onResize 改变了 current object(当前对象)(可能性并不小,因为 onResize 是一个 non-const member function(成员函数)),current object(当前对象)并不会改变。作为替代,那个 object 的一份 copy(拷贝)被改变。然而,如果 SpecialWindow::onResize 改变了 current object(当前对象),current object(当前对象)将被改变,导致的境况是那些代码使 current object(当前对象)进入一种病态,没有做 base class(基类)的变更,却做了 derived class(派生类)的变更。
解决方法就是消除 cast(强制转型),用你真正想表达的来代替它。你不需要哄骗编译器将 *this 当作一个 base class object(基类对象),你需要在 current object(当前对象)上调用 onResize 的 base class version(基类版本)。就是这样:
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // call Window::onResize
... // on *this
}
...
};
这个例子也表明如果你发现自己要做 cast(强制转型),这就是你可能用错误的方法处理某事的一个信号。在你想用 dynamic_cast 时尤其如此。