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

1.前言

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

不幸的是,转型(cast)破坏了类型系统(type system)。那可能导致任何种类的麻烦,有些容易识别,有些容易隐晦。如果你来自c,java,c#阵营,请特别注意,因为那些语言中的转型(cast)比较必要而无法避免,相对来说也不危险,但c++中转型是一个你会想带着极大的尊重去亲近的一个特性。

2.转型(cast)知识点的回顾

首先,让我们回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作。c风格的转型动作看起来像这样:

(T)expression//将expression转型为T
函数风格的转型动作看起来像这样:
T(expression)//将expression转型为T

两种形式并无差别,纯粹只是小括号的摆放位置不同而以。称此两种形式为“旧式转型”。

C++还提供四种新式转型(常被称为c++ style casts)

const_cast(expression)
dynamec_cast(expression)
reinterpret_cast(expression)
static_cats(expression)

上述转换类型各有不同的作用:

const_cast通常被用来将对象的常量性转除(cast away the constness),它也是唯一有此能力的c++-style转型操作符;

dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作;

reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也表示它不可移植。例如将一个pointer to int 转型为int。这一类转型在低级代码以外很少见。本来只使用一次,那是在讨论如何针对原始内存写出一个调试用的分配器;

static_cast用来强迫隐式转换,例如将non-cast对象转为const对象,或将int转为double等等。它也可以用来执行上述多种类型的反转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const_cast能办到;

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

(1)很容易在代码中被识别出来;

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

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

class Widget{

    public:
    explict Widget(int size);
    ...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));//以一个Int加上“函数风格”的转型动作创建一个Widget
doSomeWork(static_cast(15));//以一个int加上“c++风格”的转型动作创建一个Widget

从某个角度来说:刻意的“对象生成”动作感觉不怎么像“转型”,所以我很可能使用函数风格的转型动作而不使用static_cast。但我要提醒下,当我们日后出错导致coredump的代码时,编写的时候我们往往觉得说的过去,所以最好是忽略自己的主管想法,始终理智地使用新式转型。

许多程序员认为,转型其实什么都没做。只是告诉编译器把某种类型看作另一种类析。这是种错误的观念,任何一个类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。比如在下面这段程序中:

int x,y;
...
double d=static_cast(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*指针值。

上述例子表明,单一对象可能拥有一个以上的地址(例如“”以base指向它时的地址和以derived指向它时的地址)。实际上一旦使用多重继承,这事情几乎一直发生者。即使在单一继承中也可能发生。虽然这还有其它含义,但至少意味你通常应该避免做出“对象在c++中如何布局”的假设。当然更不应该以此为假设执行任何转型动作。例如将对象地址转型为char*指针然后在它们身上进行指针算术,这样总会导致无定义(不明确)行为。

但请注意,我说的是有时候需要一个偏移量。对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着由于知道对象如何布局而设计的转型,在某一平台行的通,再另一平台并不一定行的通。

另一件关于转型的有趣的事情是:我们很容易写出某些似是而非的代码。比如许多应用框架都要求derived class内的virtual函数代码的第一个动作就是先调用base class的对应函数。假设我们有个Window base class和一个SpecialWindow derived class,两者都定义了virtual函数的onResize,进一步假设SpeciaWindow的onResize函数被要求首先调用Window的onResize,下面是实现方式之一,相关程序看起来是对的,实际上是有问题的:

class Window{
    public:
        virtual void onResize(){
        ....//base onResize实现代码
    }

};

class SpecialWindow:public Window{//derived class
    public:
        virtual void onResize(){        //derived onResize实现代码
        static_cast(*this).onResize();//将*this转型为Window,然后调用其                                    //onResize;这不可行。

        ...//这里进行    SpecialWindow专属行为
    }




}

上面代码中强调了转型动作(那是个新式转型,但若使用旧式转型也不能改变以下事实)。如预期的那样,这段程序将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但实际上,它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“this对象之base class成分”的暂时副本身上的onResize。再强调一次,上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作,它是在“当前对象之base class成分”的副本上调用Window::onResize,然后在当前对象身上执行SpeciaWindow专属动作。如果Winodw::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使得当前对象进入一种"伤残"状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。

解决的方法是换掉转型动作,而不是将*this视为一个base class对象,你只是想调用base class版本的onResize函数,令它作用于当前对象身上。所有这样编写代码:

class SpecialWindow:public Window{

    public:
        virtual void onResize(){

        Window::onREsize();//调用Window::onResize作用于*this身上
        ...
    }
        ...
};

这个例子也说明:如果自己打算转型,可能会面临者将局面发展至错误的方向上。如果用的是dynamic_cast更是如此

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

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

第一:使用容器并在其中存储直接指向derived class对象的指针,如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,试着不要这样做:

class Window{...};
class SpecialWindow:public Window{

    public:
        void blink();
        ...
};

typedef
std::vector>  VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPTRS.begin();iter!=winPtrs.end();++iter){//不希望使用//dynamic_cast
    if(SpecialWindow* psw=dynamic_cast(iter->get())){
        psw->blink();
    }
}

应该改为这样做:

typedef std::vector> VPSW;
VPSW winPtrs;
...

for(VPSW::iterator iter=winPTRS.begin();iter!=winPtrs.end();++iter){
    (*iter)->blink();//这样写比较好,不需要使用dynamic_cast
}

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

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

class Window{

    public:
        virtual blink()=0;
        ...
}
class SpecialWindow:public Window{

    public:
        override blink(){
            ....
     }
        ...
};
typedef std::vector> VPW;
VPW winPtrs;//容器,内含指针,指向可能的Window类型
...
for(VPW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){

    (*iter)->blink();//注意,这里没有dynamic_cast。
}

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

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

class Window{
    ...
};
...//derived classes定义在这里
typedef std::vector> VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){
    if(SpecialWindow1* psw1=dynamic_cast(iter->get())){...}
    else if(SpecialWindow2* psw2=dynamic_cast(iter->get())){...}
    else if(SpecialWindow3* psw3=dynamic_cast(iter->get())){...}
    ...
}

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

良好的c++代码很少使用转型,但若说要完全摆脱它们又不切实际,例如将int转型为double就是转型的一个通情达理的使用,虽然它并非绝对必要,就像面对众多蹊跷可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何动作影响。

3.总结

由于本文内容较多,将以上内容总结为以下几点:

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

2.如果转型是必要的,试着将它隐藏在某个函数背后。客户随后可以调用该函数,而不需要将转型放进它们的代码内;

3.一旦不得不使用转型,宁可使用c++style新式转型,不要使用旧式转型。前者很容易辨识出来,而且也相对职责分明。

你可能感兴趣的:(c++,数据结构)