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

1、关于C++的转型动作

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

不幸的是,转型破坏了类型系统。那可能导致任何种类的麻烦,有的容易辨识,有些非常隐晦。C、java、c#语言中可能转型是必要的、无法避免的,相比于C++也比较不那么危险。但是C++中,应该尽量少的做转型,C++中使用转型比较危险,应该尽量将转型动作使用不转型的手法给化解掉。

2、C和C++的三种形式的转型语法

(1)形式一:C语言风格的转型语法:
 (T)expression     //将expression转换为T类型
(2)形式二:函数风格的转型:
T(expression)     ////将expression转换为T类型
(3)形式三:C++风格的转型语法
  • const_cast<T>(expression);//const->non const
    const_cast 用来将对象的const属性去掉,功能单一,使用方便,呵呵.
  • dynamic_cast<T>(expression);
    dynamic_cast 用于继承体系下的"向下安全转换",通常用于将基类对象指针转换为其子类对象指针,它也是唯一一种无法用旧式转换进行替换的转型,也是唯一可能耗费重大运行成本的转型动作.
  • reinterpret_cast<T>(expression);
    低级转型,结果依赖与编译器,这因为着它不可移植,我们平常很少遇到它,通常用于函数指针的
    转型操作.
  • static_cast<T>(expression);
    static_cast 用来进行强制隐式转换,我们平时遇到的大部分的转型功能都通过它来实现.例如将int转换为double,将void*转换为typed指针,将non-const对象转换为const对象,反之则只有const_cast能够完成.

注意:形式一、二并无差别,统称旧式转型,形式三称为新式转型。

3、倡导使用新式转型、旧式转型的唯一适用场景( 对于作者本人来说的唯一)

(1)新式转型的优点
  • 在代码这种容易被识别出来(无论是人工识别还是使用工具如grep),因而简化“找出类型系统在哪个点被破坏”的过程(简化找错的过程)。
  • 各种转型动作的目标越窄化,编译器越能判断出出错的运用。例如:如果你打算将常量性去掉,除非使用新式转型的const_cast否则无法通过编译
(2)旧式转型的唯一适用场景( 对于作者本人来说的唯一)

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

  class Widget{
    public:
        explicit Widget(int size);
        ...
    };
    void doSomething(Widget& w);
    doSomething(Widget(15)); //"旧式转型"中的函数转型
    doSomething(static_cast<Widget>(15));//"新式转型"
(3)为什么这里使用函数风格的转型呢?

因为蓄意的“对象生成”动作不怎么像“转型”,因此没使用新式转型。但是,其他情况下,即使觉得旧式转型合理,也最好使用新式转型。

4、消除一个误解:转型动作其实什么也没做

(1)误解

请不要认为转型什么都没做,其实就是告诉编译器把某种类型视为另一种类型。实际上,任何一种转型动作往往真的令编译器额外地编译出运行期间执行的代
码。
例如将int转型为double就会发生这种情况,因为在大部分的计算器体系结构中,int的底层表述不同于double的底层表述。

(2) 转型动作导致编译器在执行期间编译出不同的码的另外一个例子

单一的对象可能拥有一个以上的地址(例如:"以base指向它"时的地址和"以Derived指向它"时的地址这时会有一个偏移量在运行期间施加在Derived身上,用以取得正确的base的指针值)。实际上一旦使用多重继承,这事几乎一直发生。即使在单一继承中也可能发生。

(2)我们应该避免作做出“对象在C++中如何布局”的假设

有了偏移量这个经验后,我们也不能做出“对象在C++中如何布局”的假设。因为对象的布局方式和它们的地址计算发式随着编译器的不同而不同,这就以为着写出"根据对象如何布局"而写出的转型代码在某一平台上行得通,在其它平台上则不一定。很多程序员历经千辛万苦才学到这堂课。

5、转型动作容易写出似是而非的代码

有些场景下,需要在派生类的virtual函数中调用基类的版本的次函数:

    class Window{
    public:
        virtual void onResize(){...}
        ...
    };
    class SpecialWindow:public Window{
    public:
        virtual void onResize(){
            static_cast<Window>(*this).onResize();//调用基类的实现代码
            ... //这里进行SpecialWindow的专属行为.
        }
        ...
    };

上述代码看着似乎合情合理,但是实际却是错误的。错在转型语句。为什么错呢?首先它确实执行了多态,调用的函数版本是正确的,但是由于做了转型,它并没有真正作用在派生类对象身上,而是作用在了派生类对象的基类部分的副本身上,改动的是副本。但是如果改动当前对象的派生类部分的话,不做转型动作就真的改变了当前对象的派生类部分。但是导致的最终结果就是:当前对象的基类部分没有被改动,但是派生类部分缺被改动了。

上述代码的正确写法:

    void SpecialWindow::onResize(){
        Window::onResize(); //此时才是真正的调用基类部分的onResize实现.
        ...     //同上
    }

6、关于dynamic_cast

首先要有一个认识,就是dynamic_cast的实现版本执行速度相当的慢。尤其是在深度继承和多重继承中,速度更慢。

7、何时需要dynamic_cast,以及避dynamic_cast的方法

(1)何时需要dynamic_cast?

通常当你想在一个你认定为derived class对象上执行derived class操作函数时,但是你的手上只有一个指向base 的指针或引用时,你会想到使用dynamic_cast进行转型

(2)如何不做转型,实现上述需求?

通常有两种做法可以解决上述问题:

  • 方法一:使用容器,并在其中存储直接指向derived class对象的指针(通常是智能指针),这样就避免了上述需求。
  • 方法二:在base class内提供virtual函数做你想对各个派生类想做的事情。这样可以使得你通过base class
    接口处理“所有可能之各种派生类”。

7、绝对避免一连串的dynamic_cast

一连串dynamic_cast的代码又大又慢,而且基础不稳,因为每次继承体系一有改变,所有这种代码必须再次进行检查看看是否需要修改。例如假如新的派生类,就要加新的分支。这样的代码应该使用“基于virtual函数调用”的东西取而代之。

8、关于类型转换最后的话

完全不用转型是不切实际的。但是我们应该尽量避免转型。就像面对众多蹊跷可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。

posted @ 2020-01-02 15:44  江南又一春  阅读(132)  评论(0编辑  收藏  举报