***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
五、Implementations
Rule 27:Minimize casting
规则 27:尽量少做转型动作
1.一些基础
C++规则的设计目标之一 —— 保证“类型错误”绝对不可能发生。
理论上,如果程序很"干净地"通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。
But,转型(cast)破坏了类型系统(type system),这种可能会导致任何种类的麻烦,而且这些麻烦繁琐度不一。
如果你用的是C、Java 或 C# ,这点就需要特别注意一下,因为那些语言中的转型比较必要而无法避免,与C++相对比较而言,也不是特别危险。
关于转型(cast),这里通常有三种不同的形式:( 都是将expression转型为T )
—— C风格的转型动作: (T)expression
—— 函数风格的转型动作: T(expression)
这两种形式没有差别,只是小括号的位置不同而已,可以称这两种形式为 " 旧式转型 "
C++ 还提供了四种新式转型(常常被称为 new-style 或者 C++-style )
—— const_cast<T> ( expression )
▪ 这种通常用来将对象的常量性转除。它也是 唯一 有此能力的C++-style转型操作符。
—— dynamic_cast<T> ( expression )
▪ 主要用来执行 ”安全向下转型 “,也就是用来决定某对象是否归属继承体系中的某个类型。 它是 唯一 无法由旧式语法执行的动作,也是 唯一 可能耗费重大运行成本的转型动作。
—— reinterpret_cast<T> ( expression )
▪ 执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。 例如:将一个 pointer to int 转型为 int。这一类转型在低级代码以外很少见。
—— static_cast<T> ( expression )
▪ 用来执行强迫隐式转换,例如:将一个 non-const 对象转换为 const 对象,或将一个 int 转为 double等等。它也可以用来执行上述多种转换的反向转换,例如:将 void* 指针转为 typed 指针,将 pointer-to-base 转为 pointer-t0-derived。但它无法将 const 转为 non-const,这个只有 const_cast才办得到。
这么多种形式的转型,虽然旧式转型仍然合法,但新式转型较受欢迎。原因有二:
> 它们很容易在代码中被辨识出来(不论是人工辨识或使用工具),因而得以简化“找出类型系统在哪个地点被破坏”的过程。
> 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
PS: 有一个 唯一 使用旧式转型的时机,当需要调用一个 explicit 构造函数将一个对象传递给一个函数时。
比如:
class Widget { public: explicit Widget( int size ); ... }; void doSomeWork( const Widget& w ); // 以一个int加上“函数风格”的转型动作创建一个Widget doSomeWork( Widget(15) ); // 以一个int加上“C++风格”的转型动作创建一个Widget doSomeWork( static_cast<Widget>(15) );
2.一些东西
① 许多程序员认为,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。But,这是错误的观念。
任何一个类型转换往往真的令编译器编译出运行期间执行的码。
例如在下面这段程序中:
int x,y; ... double d = static_cast<double>(x)/y; // x除以y,使用浮点数除法
将int转型为double几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int的底层表述不同于double的底层表述。
但在下面这个例子,尤其需要注意一下:
class Base { ... }; class Derived : public Base { ... }; Derived d; Base* pb = &d; // 隐喻的将Derived* 转换为 Base*
在这里,不过是建立一个 基类的指针 指向一个 派生类的对象,但有时候上述两个指针值并不相同。这种情况下会有一个 偏移量 在运行期被施行于 派生类指针身上,用以取得正确的 基类指针值。
上面这个例子已经表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(比如 以"Base* 指向 " 和 以" Derived* 指向" 时的地址),在C、Java、C#中都不可能发生这种事,唯独C++中可以,实际上一旦使用多重继承,它就一直发生,即使是单一继承中也可能发生。虽然这里面可能有些其他的东西,但至少意味着我们应该避免做出"对象在C++中如何如何布局"的假设,更不该以此假设为基础执行任何转型动作。
② 我们很容易写出某些似是而非的代码(即使是在别的语言中)。
比如,许多应用框架都要求 派生类内的 虚函数 代码的第一个动作就先调用基类的对应函数。假设我们有个 Window base class 和一个 SpecialWindow derived class,两者都定义了 virtual函数 onResize。进一步假设 SpecialWindow 的 onResize 函数被要求收先调用Window的onResize。下面是一种实现方法(似是而非的)
class Window { // 基类 public: virtual void onResize() { ... } ... }; class SpecialWindow : public Window { // 派生类 public: virtual void onResize() { static_cast<Window>(*this).onResize(); // 将*this转型为Window,然后调用其onResize ,这样是错的 ... } ... };
稍微解释一下,在此代码中强调了转型动作(此处用了新式转型)。这段程序将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但它调用的并不是当前对象的函数,而是稍早转型动作所建立的一个"*this对象的基类成分"的暂时副本上的onResize。
▪ 这个问题的解决方法就是——拿掉转型动作,直说。
比如,如果只是想调用 基类 版本的onResize函数,令它作用于当前对象身上。所以这么写:
class SpecialWindow : public Window { public: virtual void onResize() { Window::onResize(); // 调用Window::onResize作用于*this身上 ... } ... };
③ 对于 dynamic_cast 有些需要注意的地方,它的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于" class名称之字符串比较 ",如果你在四层深的单继承体系内的某个对象身上执行 dynamic_cast ,刚才说的那个实现版本所提供的每一次 dynamic_cast 可能会好用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!
什么时候用 dynamic_cast 呢?用dynamic_cast通常是因为你想在一个认定的 派生类 对象身上执行 派生类操作函数,但手上却只有一个 指向基类 的指针或引用。
这里有两个做法来避免这个问题
> 使用容器并在其中存储直接指向派生类对象的指针(通常是智能指针),如此便消除了"通过基类接口处理对象"的需要。
用之前Window的例子,如果Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,试着 不要 这样做:
class Window { ... }; class SpecialWindow : public Window { public: void blink(); ... }; typedef std::vector<std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; ... for( VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter ) { if( SpecialWindow* psw = dynamic_cast<SpecialWindow* >( iter->get() ) ) psw->blink(); }
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW; VPSW winPtrs; ... for( VPSW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter ) { // 没使用dynamic_cast (*iter)->blink(); }
> 另一种做法可以 通过基类接口处理"所有可能之各种Window派生类",那就是在基类内提供virtual函数做你想对各个Window派生类做的事。接着用这个例子,虽然只有SpecialWindow可以闪烁,但或许将闪烁函数声明于基类内并提供一份"什么也没做"的缺省实现码是有意义的:
class Window { public: virtual void blink() { } // 缺省实现代码 ... }; class SpecialWindow : public Window { public: virtual void blink() { ... }; // 在SpecialWindow类内,blink函数做一些动作 ... } typedef std::vector< std::tr1::shared_ptr<Window> > VPW; // 内含指针的容器,指向所有可能的Window类型 VPW winPtrs; ... for( VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter ) // 这里没使用 dynamic_cast (iter*)->blink();
3.注意
一定要避免一件事——连串 cascading dynamic_casts
也就是,类似这样的事情:
class Window { ... }; ... // 派生类在这里定义 teypdef std::vector<std::tr1::shared_ptr<Window> > VPW; VPW winPtrs; ... 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()) ) { ... } ... }
例如:一旦加入新的派生类,或许上述所有的连串判断中需要加入新的条件分支。这样的代码应该被“基于virtual函数调用”的取代。
4.总结
优良的C++代码很少使用转型,但若要说完全摆脱它们又不太可能。有很多转型是通情达理的,虽然有些并不是必须那样做。如同面对多种的构造函数那样,我们应该尽可能的少用转型动作,通常会把它隐藏在某个函数内,函数的接口会保护调用的人不受函数内部的干扰。
★ 请记住 ☆
▪ 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
▪ 如果转型是必要的,试着将它隐藏于某个函数的背后。用户随后可以调用该函数,而不需要将转型放进他们自己的代码内。
▪ 宁可使用C++-style转型,不要使用旧式转型。新式转型容易辨识,并且有着分门别类的支持。