尽量少做转型动作——条款27

        C++规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上如果你的程序很“干净地”通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。这是一个极具价值的保证,可别草率地放弃它。

        不幸的是,转型(casts)破坏了类型系统(type system)。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。让我们首先回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作,C风格的转型动作看起来像这样:

        (T)expression        // 将expression转型为T

        函数风格的转型动作看起来像这样:

        T(expression)        // 将expression转型为T

        两种形式并无差别,纯粹只是小括号的摆放位置不同而已。我称此二种形式为“旧式转型”(old-style casts)。

        C++还提供四种新式转型(尝尝被称为new-style或C++-style casts):

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

        各有不同的目的:

  • const_cast:通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++ style转型操作符。
  • dynamic_cast:主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作(稍后细谈)。
  • reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类型在低级代码以外很少见。
  • static_cast:用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象(就像条款3所为),或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才办得到。

        旧式转型仍然合法,但新式转型较受欢迎。原因是:

        第一,它们很容易在代码中被辨识出来(不论是人工辨识或使用工具如grep),因而得以简化“找出类型系统在哪个地点被破坏”的过程。

        第二,各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。举个例子,如果你打算将常量性(constness)去掉,除非使用新式转型中的const_cast否则无法通过编译。

        我唯一使用旧式转型的时机是,当我要调用一个explicit构造函数将一个对象传递给一个函数时。例如:

class Widget {
public:
	explicit Widget(int size);
	...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));        // 以一个int加上“函数风格”的转型动作创建一个Widget。
doSomeWork(static_cast<Widget>(15));    // 以一个int加上"C++ 风格"的转型动作创建一个Widget。

        从某个角度来说,蓄意的“对象生成”动作感觉不怎么像转型“转型”,所以我很可能使用函数风格的转型动作而不使用static_cast。撰写之时我们往往“觉得”通情达理,所以或许最好是忽略你的感觉,始终理智地使用新式转型。

        许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。任何一个类型转换(不论是通过转型操作而进行的显示转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间的执行代码。

例如这段程序:

int x, y;
...
double d = static_cast<double>(x)/y;  // 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*指向它”时的地址。)行为。

        但请注意,我说的是有时候需要一个偏移量。对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味“由于知道对象如何布局”而设计的转型,在某一平台行得通,在其它平台并不一定行得通。这个世界有许多悲惨的程序员,他们历经千辛万苦才学到这堂课。

        在探究dynamic_cast设计意涵之前,值得注意的是,dynamic_cast的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于“class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,刚才说的那个实现版本所提供的每一次dynamic_cast可能会好用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!

        之所以需要dynamic_cast,通常是因为你想在一个你认定为Derived class对象身上执行Derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。

        第一,使用容器并在其中存储直接执行Derived class对象的指针(通常是智能指针,见条款13),如此便消除了“通过base class接口处理对象”的需要。   例如:

class Window {...};
class SpecialWindow: public Window {
public:
	void blink();
	...
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPM; // 关于tr1::shared_ptr 见条款13
VPM winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
	if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))    // 不希望使用dynamic_cast
		psw->blink();
}
        
应该改成这样:
typedef std::vector<std::tr1::shared_ptr<Window>> VPM; // 关于tr1::shared_ptr 见条款13
VPM winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
	// 这样写比较好,不使用dynamic_cast
	(*iter)->blink();
}

        这种做法使你无法在同一个容器内存储指针“指向所有可能之各种Window派生类”。如果真要处理多种窗口类型,你可能需要多个容器,它们都必须具备类型安全性(type-safe)。

        另一种做法可让你通过base class接口处理“所有可能之各种Window派生类”,那就是在base class内提供virtual函数做你想对各个Window派生类做的事。例如,上述例子中虽然只有SpecialWindows可以闪烁,但或许闪烁函数声明base class内并提供一份“什么也没做”的缺省实现码是有意义的:

class Window {
public:
	virtual void blink() {}  // 缺省实现代码”什么也没做“;
	...                      // 条款34告诉你为什么缺省实现代码可能是个馊主意。
};
class SpecialWindow: public Window {
public:
	virtual void blink();    // 在此class内,blink做某些事。
	...
};
typedef std::vector<std::tr1::shared_ptr<Window>> VPM; 
VPM winPtrs;                                           // 容器,内含指针,指向所有可能的Window类型。      
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
	// 注意,这里没有dynamic_cast
	(*iter)->blink();
}

        无论哪一种写法——“使用类型安全容器”或“将virtual函数往继承体系上方移动”——都并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。当它们由此功效时,你应该欣然拥抱它们。

        绝对必须避免一件事是所谓的“连串(cascading)dynamic_casts”,也就是看起来像这样的东西:

class Window {...};
...                  // derived classes定义在这里
typedef std::vector<std::tr1::shared_ptr<Window>> VPM; 
VPM winPtrs;                                           // 容器,内含指针,指向所有可能的Window类型。      
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
	if (SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1>(iter->get())){...}
	else if (SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2>(iter->get())){...}
	else if (SpecialWindow3 *psw3 = dynamic_cast<SpecialWindow3>(iter->get())){...}
	...
}

        这样的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有改变,所有这一类代码都必须再次检阅是否需要修改。一旦假如新的Derived class,或许上述判定中要加入新的条件分支。这样的带应该总是以某些“基于virtual函数调用”的东西取而代之。

        优良的C++带很少使用转型,但若说要完全摆脱它们又太过不切实际。我们应该尽可能隔离转型动作,通常把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何动作影响。

请记住

  • 如果可以,尽量避免转型,特别是注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C++ style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值