Fork me on GitHub

读书笔记 effective c++ Item 27 尽量少使用转型(casting)

 

C++设计的规则是用来保证使类型相关的错误不再可能出现。理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作。这个保证很有价值,不要轻易放弃它。

不幸的是,casts颠覆了类型系统。它导致了各种麻烦的出现,一些很容易识别,一些却很狡猾(不容易被识别)。如果你以前使用过C,java或者C#,你就需要注意了,因为在这些语言中casting是更加必不可少的,但却比C++更安全。C++不是C,不是java也不是C#,在C++中,你需要怀着极大的敬意来使用casting。

1. 新旧风格cast回顾

1.1 旧风格cast

先让我们回顾一下casting的语法,通常有三种不同的方法来实现同一个cast。C风格的casts如下:

1 (T) expression // cast expression to be of type T

 

函数风格的casts使用下面的语法:

1 T(expression) // cast expression to be of type T

 

上面的两种形式在意义上没有区别,只是放括号的地方不一样。我们将这两种形式的casts叫做旧式风格的casts。

1.2 C++风格的cast

C++同样提供四种新的casts形式(C++风格的casts):

1 const_cast<T>(expression)
2 dynamic_cast<T>(expression)
3 reinterpret_cast<T>(expression)
4 static_cast<T>(expression)

 

每种方法都有独特的用途:

  • Const_cast是用来去除对象的常量性的(constness)。在四个C++风格的cast中,const_cast是唯一能做到这一点的。
  • Dynamic_cast主要用来执行“安全的向下转型”,也就是决定一个特定类型的对象是否在一个继承体系中。这也是唯一一个不能用旧式风格语法来实现的cast它也是唯一一个可能会出现巨大的运行时开销的cast。(稍后会详细讲解)
  • Reinterpret_cast被用来做低级的casts,结果可能依赖于编译器,也就是代码不能被移植,例如,将一个指针转换成int。这种casts除了用在低级代码中,其他地方很少见。本书中只出现过一次,就是在讨论如何为原生内存(raw memory)实现一个调试分配器(Item 50)。
  • Static_cast能被用来做强制的显示类型转换(比如,non-const对象转换成const对象(Item 3),int转换成double等等。)它同样能够用来对这些转换进行反转(比如,void*转换成具体类型指针,指向base的指针转换成指向派生类的指针),但是不能从const转换成非const对象(只有const_cast能这么做)。

1.3 旧风格PK新风格

旧式风格的casts仍然合法,但是新风格的更好。第一,在代码中它们更加容易被辨别(对于人或者工具来说),因此简化了在代码中寻找转型动作的过程。第二,每个cast更加特别的使用用途使得编译器能够诊断出使用错误成为可能。譬如,如果你使用其它3个cast而不是const_cast来去除常量的常量性,你的代码无法通过编译。

我使用旧式风格cast的唯一地方是当我想通过调用一个explici构造函数来为一个函数传递一个对象的时候。比如:

 1 class Widget {
 2 public:
 3 explicit Widget(int size);
 4 ...
 5 };
 6 void doSomeWork(const Widget& w);
 7 doSomeWork(Widget(15));                     // create Widget from int
 8 // with function-style cast
 9 
10 doSomeWork(static_cast<Widget>(15));      // create Widget from int
11 // with C++-style cast

 

从某种意义上来说,这种对象的创建不像是一个cast,所以使用了函数风格的cast而不是static_cast。(这两种方法做了相同的事情:创建一个临时Widget对象然后传递给doSomeWork。)需要再说一遍,使用旧式转型实现的代码往往当时感觉很合理,但日后可能出现core dump,所以最好忽略这种感觉,总是使用新风格的casts。

2. 使用cast会产生运行时代码——不要认为你以为的就是你以为的

许多程序员认为cast除了告诉编译器需要将一个类型当作另外一个类型之外,没有做任何事情,但这个一个误区。任何种类的类型转换(不管显示cast还是隐式转换)都会产生运行时代码。举个例子:

1 int x, y;
2 ...
3 double d = static_cast<double>(x)/y; // divide x by y, but use
4 // floating point division

 

将int x转换成double肯定会产生代码,因为在大多数系统架构中,int的底层表示同double是不一样的。这也许不会让你吃惊,但下面的例子可能亮瞎你的双眼:

1 class Base { ... };
2 class Derived: public Base { ... };
3 Derived d;
4 
5 Base *pb = &d;                                    // implicitly convert Derived* ⇒ Base*

 

这里我们只是创建了一个指向派生类对象的基类指针,但有时候,这两个指针(Derived*和Base*)值将会不一样。在上面的情况中,运行时会在Derived*指针上应用一个偏移量来产生正确的Base*指针值。

 

最后这个例子表明一个对象(比如Derived类型的对象)可能有多于一个的地址(比如,当Base*指针指向这个对象和Derived*指向这个对象时有两个地址)。这在C,java和C#中不可能发生。事实上,当使用多继承时,这种情况总会发生,但在单继承中也能发生。这意味着在C++中你应该避免对一些东西是如何布局的做出假设。例如,将对象地址转换成char*指针然后在此指针上面进行指针算术运算几乎总是会产生未定义行为

 

但是注意我说过偏移量“有时候“是需要的。对象的布局方式和地址被计算的方式会随编译器的不同而不同。这意味着仅仅因为你了解一种平台上的布局和转型并不意味着在别的平台上也能如此工作。世界上充满了从中吸取教训的悲哀的程序员。

 

3. Cast很容易被误用——无效状态是如何产生的

 

关于cast的一件有趣的事情是容易写出看上去正确但实际错误的代码。比如,许多应用框架需要派生类中的虚函数实现首先要调用基类部分。假设我们有一个Window基类和一个SpecialWindow派生类,两个类中都定义了onResize虚函数。进一步假设SpecialWindow的onResize函数首先要调用Window的onResize函数。下面的实现方式看上去正确,实际上并非如此:

 1 class Window {                       // base class
 2 
 3 public:                                   
 4 
 5  
 6 
 7 virtual void onResize() { ... }           // base onResize impl
 8 
 9 ...                                                   
10 
11 };                                                   
12 
13  
14 
15 class SpecialWindow: public Window {       // derived class
16 
17 public:                                                       
18 
19 virtual void onResize() {                             // derived onResize impl;
20 
21 static_cast<Window>(*this).onResize();     // cast *this to Window,
22 
23 
24 // then call its onResize;
25 // this doesn’t work!
26 ... // do SpecialWindow-
27 } // specific stuff
28 ...
29 
30 
31 };

 

我已经对代码中的cast标注了红色。(它是新风格的cast,使用旧风格的转换也不会改变如下事实)。正如你所期望的,代码将*this转换成一个window对象。因此调用onResize时会触发Window:: onResize。你可能想不到的是它并没有在当前的对象上触发相应的函数。相反,转型动作为*this的基类部分创建了一份新的临时拷贝,onResize是在这份拷贝上被触发的!上面的代码没有在当前对象上调用Window::onResize然后在此对象上执行SpecialWindow的指定动作——它在执行特定动作之前,在当前对象基类部分的拷贝之上调用了Window::onResize。如果Window::onResize修改了当前对象(很有可能,既然onResize是non-const成员函数),当前的对象(Window对象)是不会被修改的。修改的是对象的拷贝。然而如果SpecialWIndow::onResize修改当前对象,当前对象将会被修改,导致上面代码会为当前对象留下一个无效状态:基类部分没有被修改,派生类部分却被修改了

解决方法是消除cast的使用,你不想欺骗编译器让其把*this当作一个基类对象。你想的是在当前对象上调用onResize的基类版本。所以按照下面的方法做:

1 class SpecialWindow: public Window {
2 public:
3 virtual void onResize() {
4 Window::onResize(); // call Window::onResize
5 ... // on *this
6 }
7 ...
8 };

 

这个例子同样表明如果你发现你自己想使用cast了,它就标志着你可能会使用错误的方式来应用它。使用dynamic_cast的时候也是如此。

4. Dynamic_cast 分析

4.1 Dynamic_cast速度很慢

在深入研究dynamic_cast的设计含义之前,我们能观察到dynamic_cast的很多实现其速度是非常慢的。举个例子,至少有一种普通的实现在某种程度上是基于类名称的字符串比较。如果你正在一个4层深的单继承体系的对象上执行dynamic_cast,在这样一种实现(也就是上面说的普通实现)下每个dynamic_cast至多可能调用四次strcmp来比较类名称。一个层次更深的继承或者一个多继承可能开销会更大。这样实现是有原因的(它们必须支持动态链接(dynamic linking))。因此,除了要对使用cast时的一般问题保持机敏,在对性能敏感的代码中更要对dynamic_cast的使用保持机敏

4.2 Dynamic_cast的两种替代方案

你需要dynamic_cast是因为你想在你坚信其是派生类对象之上执行派生类操作,但你只能通过基类指针或基类引用来操作此对象。有两种普通的方法避免使用dynamic_cast

第一,  使用容器直接存储派生类对象指针(通常情况下使用智能指针,见Item 13),这样就消除了通过基类接口来操纵这些对象的可能。举个例子,在我们的window/SpecialWindow继承体系中,只有SpecialWindows支持blink,不要像下面这样做:

 

 1 class Window { ... };
 2 class SpecialWindow: public Window {
 3 public:
 4 void blink();
 5 ...
 6 };
 7 
 8 typedef                                                                                              // see Item 13 for info
 9 
10 std::vector<std::tr1::shared_ptr<Window> > VPW; // on tr1::shared_ptr     
11 
12 VPW winPtrs;                                                                                   
13 
14 ...                                                                                                       
15 
16 for (VPW::iterator iter = winPtrs.begin();                                          // undesirable code:
17 
18 iter != winPtrs.end();                                                                         // uses dynamic_cast
19 
20 ++iter) {                                                                                            
21 
22 if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))    
23 
24 psw->blink();                                                                                   
25 
26 }   

                                             

 而是用下面的做法:

 1 typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
 2 
 3 VPSW winPtrs;
 4 
 5 ...
 6 
 7 
 8 for (VPSW::iterator iter = winPtrs.begin(); // better code: uses
 9 iter != winPtrs.end(); // no dynamic_cast
10 ++iter)
11 (*iter)->blink();

 

当然这种方法不允许你在同一个容器中存储所有可能的Window派生物。要达到这个目的,你可能需要多个类型安全的容器。

第二,  在基类中提供虚函数。举个例子,虽然只有SecialWindos支持blink,你同样可以在基类中声明一个blink,但默认实现是什么都不做:

 1 class Window {
 2 public:
 3 virtual void blink() {} // default impl is no-op;
 4 ... // see Item 34 for why
 5 }; // a default impl may be
 6 // a bad idea
 7 class SpecialWindow: public Window {
 8 public:
 9 virtual void blink() { ... } // in this class, blink
10 
11 ...
12 
13 // does something
14 
15 };
16 
17  
18 
19 typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
20 
21  
22 
23 
24 VPW winPtrs; // container holds
25 // (ptrs to) all possible
26 ... // Window types
27 for (VPW::iterator iter = winPtrs.begin();
28 iter != winPtrs.end();
29 ++iter) // note lack of
30 (*iter)->blink(); // dynamic_cast

 

上面的两种方法不是在任何情况下都能使用,但是在许多情况下,它们为dynamic_cast提供了一种可行的替代方案。当他们确实能做到你想要的,你应该拥抱它们。

4.3 不要在级联设计中使用dynamic_cast

你绝对想避免的一件事是不要做包含级联dynamic_cast的设计,也就是像下面这个样子:

 1 class Window { ... };
 2 
 3 ...
 4 
 5 // derived classes are defined here
 6 
 7 typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
 8 
 9  
10 
11 VPW winPtrs;
12 
13  
14 
15 ...
16 
17  
18 
19 for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
20 
21  
22 
23 {
24 
25  
26 
27 if (SpecialWindow1 *psw1 =
28 
29  
30 
31 dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
32 
33  
34 
35 else if (SpecialWindow2 *psw2 =
36 
37  
38 
39 dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
40 
41  
42 
43 else if (SpecialWindow3 *psw3 =
44 
45  
46 
47 dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
48 
49  
50 
51 ...
52 
53  
54 
55 }

 

这种实现产生的代码既大又慢,也很脆弱,因为每次Windos类体系发生变化,你都需要为上面的代码做一次检查是否需要更新。(例如,如果添加了一个新的派生类,上面的代码可能需要添加一个新的条件分支)。这样的代码应该被基于虚函数的设计替换掉。

 

5. 把对cast的使用隐藏在函数接口中

 

好的C++ 代码很少使用casts,但完全去除它们也是不切实际的。Int 转换成double这样的cast是合理的应用,虽然有可能不是必须的。(可以重新声明一个新的double变量,用x的值来对其进行初始化)。像许多可能令人起疑的设计一样,要尽可能的对cast的使用进行隔离,可以将其隐藏在调用者看不见的接口中。

 

6. 总结: 

    • 能避免就避免使用cast,尤其在对性能敏感的代码中对使用dynamic_cast要谨慎。如果一个设计需要cast,首先尝试是否能设计出一个不需要cast的替代方案。
    • 当必须使用casting的时候,尽量将其隐藏在函数中。客户可以调用这个函数从而避免在他们自己的代码中使用casts
    • 优先使用C++风格的cast而不是旧式风格的casts。因为它们很容易被看到,它们做的事情也更加明确。
posted @ 2017-03-04 10:20  HarlanC  阅读(999)  评论(0编辑  收藏  举报