28.尽量避免转型操作

(本节内容联系More Effective C++ 5 效果更佳)

前言

 
c++规则的设计目标之一就是:保证类型错误绝不可能发生。理论上而言,如果你的程序能很干净地通过编译,就说明了它并不企图在任何对象身上执行任何不安全、无意义、荒谬的操作。
但转型(cast)破坏了类型系统。在Java,C#,或C中,转型操作必要且较为安全(当然,安全是相对的),C++的转型则充斥着风险。


转型语法

旧式转型

1
2
(T)expression;//将expression转为T
T(expression);//将expression转为T

两者并无差别,无非是小括号的位置不同。

C++转型

C++提供了四种新式转型操作符:

  • const_cast(expression)
  • dynamic_cast(expression)
  • reinterpret_cast(expression)
  • static_cast(expression)

C++转型操作符介绍

  • const_cast:能够将对象的const属性去除,也只有它拥有该能力
  • dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系的某个类型。它是唯一一个无法由旧式语句转换的新类型转换,也是唯一一个可能耗费大量运行成本的转型操作。
  • reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这一特性也就意味着不可移植。比如将一个pointer to int转为int,该转型操作在低级代码以外相当少见。
  • static_cast:强迫隐式转换,比如把non-const转为const,int转为double。它也可以作上述转换的反向转换,例如把pointer-to-base转为pointer-to-derived,或者把void*指针转为typed指针。但是没法把const转为non-const。

新旧转型的优劣以及使用时机

新式转型的优点

尽管旧式转型依然合法,但新式转型更受青睐。原因在于:

  1. 易于辨识
  2. 转型动作明确

何时采用旧式转型

一般来说,仅在需要调用一个explicit构造函数将一个对象传递给一个函数时使用旧式转型:

1
2
3
4
5
6
7
8
class Widget{
public:
explicit Widget(int size);
...
};
void doSth(const Widget& w);
doSth(Widget(15));
doSth(static_cast<Widget>(15));

然而这种风格并不像是转型,更像是一种构造。但必须明确,这里其实执行的类型转换操作。


类型转换的意义

 
有人认为转型只不过告诉编译器把某种类型视为另一种类型,其他什么都没做,实际上这是不可能的
。将int转为double的时候编译器肯定生成了一些代码,因为int的底层描述与double的底层描述并不相同。再比如说:

1
2
3
4
class Base{...};
class Derived:public Base{...};
Derived d;
Base* pb = &d;//暗中将Derived*转为了Base*

我们建立了一个base指针指向derived class对象,但有时上述的两个指针值并不相同,这时会有一个偏移量在运行期间被施行于derived*指针,以取得正确的base*值。

上述案例表明,单一对象可能拥有多个地址(这在C,Java,C#中是不可能发生的)。如果在C++中使用多重继承,单一对象必然拥有多个地址,并且在单一继承下也可能存在多个地址。我们应该避免做出“对象在c++中如何布局”的假设,更不应该以某种假设为基础执行任何转型操作。


类型转换的误区

 
类型转换可能会导致我们易写出一些似是而非的代码,(它们在其他语言中可能是正确的)。

问题实例

许多应用框架都要求derived classes内的virtual函数第一件事就是调用base class的对应函数。下面给出一个具体实例:

1
2
3
4
5
6
7
8
9
10
11
class Window{
public:
virtual void onResize() {...}
};
class SpecialWindows:public Window{
public:
virtual void onResize(){//要求首先调用base class的对应函数
static_cast<Window>(*this).onResize();//error!
...//执行专有操作
}
}

我们确实成功地将它转为了Window对象,也确实调用了Window::onResize。但值得注意的是,调用的并不是当前对象上的函数,而是稍早转型动作建立的一个“*this对象之base class成分”的暂时副本身上的onResize(),也就是说当前对象内部的数据并没有改变,改变的是一个副本。(这个原理很简单,就是转型操作导致了一个临时Window对象被生成,我们在该临时对象身上调用了onResize,伴随着它的析构,这个操作就像没做一样,(*this)的bc成分根本没有改变)

解决方案

解决方案就是直接调用,不要使用转型操作:

1
2
3
4
5
6
7
class SpecialWindows:public Window{
public:
virtual void onResize(){
Window::onResize();
...//执行专有操作
}
}


dynamic_cast探究

 
首先需要强调的是,dynamic_cast执行速度相当缓慢,如果继承层次很深或者存在多继承的情况下使用dynamic_cast,可能对性能的冲击极大,请谨慎使用!

dynamic_cast功能

之所以需要使用dynamic_cast,主要是因为我们需要在一个derived class对象身上执行derived class专属操作,但偏偏我们目前只有一个指向base的pointer或reference.

如何避免使用dynamic_cast

使用容器,并指定其存储的元素类型为指向derived class对象的指针

举例而言,假设在之前的体系中,SpecialWindow有一个特有操作:闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Window{...};
class SpecialWindows:public Window{
public:
void blink();
...
};
//需要闪烁所有可以闪烁的窗口
typedef vector<shared_ptr<Window> > VPW;
VPW winPtrs;
...
//使用dynamic_cast
for(auto it=winPtrs.begin();it!=winPtrs.end();++it){
//赋值,检测其返回值不为nullptr
if(SpecialWindow *psw = dynamic_cast<SpecialWindows*>(iter->get()))
psw->blink();
}

应该改为在容器内存在指向dc的smart_ptr:
1
2
3
4
5
6
typedef vector<shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for(auto it=winPtrs.begin();it!=winPtrs.end();++it){
(*iter)->blink();
}

不过,这种做法无法在同一个容器内存储指向所有派生类的指针。

通过base接口处理所有可能的派生类

具体来说,就是在基类中声明所有派生类所具备的特殊函数,然后提供一份空白实现。(这种方法十分恶劣,违背了面向对象准则)

dynamic_cast禁区

绝对必须避免的是连锁式使用dynamic_cast,其运行效率极低且缺乏维护性。这种程序总应该被某种基于virtual函数调用的方法取而代之。


总结

 
优秀的c++代码很少使用转型,但完全摆脱它们也不切实际。我们要做的只能是尽可能隔绝转型动作,把他们隐藏在某个函数内。