《Effective C++》学习笔记——条款27

来源:互联网 发布:三星s4拍照软件 编辑:程序博客网 时间:2024/05/03 14:51

***************************************转载请注明出处: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加上“函数风格”的转型动作创建一个WidgetdoSomeWork( Widget(15) );// 以一个int加上“C++风格”的转型动作创建一个WidgetdoSomeWork( 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身上    ...  }  ...};

在这个例子中,可以发现:如果想用转型这个动作,这就等于一个warning,因为可能正将局面发展至错误的方向上,尤其是当使用dynamic_cast的时候。

③ 对于 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派生类"。如果真要处理多种窗口类型,这就可能需要更多的容器,它们都必须具备类型安全性。

> 另一种做法可以 通过基类接口处理"所有可能之各种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();

不论哪一种写法——" 使用类型安全容器"或"将virtual函数往继承体系上方移动"——都并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。




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()) )  { ... }  ...}

这样的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有所改变,所有这一类代码都必须再次检阅看看是否需要修改。

例如:一旦加入新的派生类,或许上述所有的连串判断中需要加入新的条件分支。这样的代码应该被“基于virtual函数调用”的取代。




4.总结

优良的C++代码很少使用转型,但若要说完全摆脱它们又不太可能。有很多转型是通情达理的,虽然有些并不是必须那样做。如同面对多种的构造函数那样,我们应该尽可能的少用转型动作,通常会把它隐藏在某个函数内,函数的接口会保护调用的人不受函数内部的干扰。

★ 请记住 ☆

▪ 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。

▪ 如果转型是必要的,试着将它隐藏于某个函数的背后。用户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

▪ 宁可使用C++-style转型,不要使用旧式转型。新式转型容易辨识,并且有着分门别类的支持。






***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
0 0
原创粉丝点击