• Effective C++条款27:尽量少做转型动作(Minimize casting)



    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

    第5章:实现

    在这里插入图片描述


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

      C++设计的目标之一是,保证“类型错误”绝不可能发生。理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作。这个保证很有价值,不要轻易放弃它。

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

    1、数据类型转型语法回顾

    1.1 C风格的cast

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

    (T) expression // 将expression转型为T
    
    • 1

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

    T(expression) // 将expression转型为T
    
    • 1

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

    1.2 C++风格的cast

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

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

      各有不同目的:

    • Const_cast是用来去除对象的常量性的(constness)。在四个C++风格的cast中,const_cast是唯一能做到这一点的。

    • Dynamic_cast主要用来执行“安全的向下转型”,也就是决定一个特定类型的对象是否在一个继承体系中。这也是唯一一个不能用旧式风格语法来实现的cast。它也是唯一一个可能会出现巨大的运行时开销的cast。(稍后细谈解)

    • Reinterpret_cast被用来做低级的转型,结果可能取决于编译器,也就是代码不能被移植,例如,将一个指针转换成int。这种casts除了用在低级代码中,其他地方很少见。本书中只出现过一次,就是在讨论如何为原生内存(raw memory)实现一个调试分配器(条款50)。

    • Static_cast能被用来做强制的显示类型转换(比如,non-const对象转换成const对象(条款3),int转换成double等等。)它同样能够用来对这些转换进行反转(比如,void*转换成具体类型指针,指向base的指针转换成指向派生类的指针),但是不能从const转换成非const对象(只有const_cast能这么做)。

    1.3 新风格转型更受欢迎

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

      我使用旧式风格转型的唯一地方是当我想通过调用一个explicit构造函数将一个对象传递给一个函数时。例如:

    class Widget {
    public:
    	explicit Widget(int size);
    	...
    };
    void doSomeWork(const Widget& w);
    doSomeWork(Widget(15));                     // 以一个int加上“函数风格”的转型动作创建一个Widget 
    doSomeWork(static_cast<Widget>(15));      // 以一个int加上C++风格的转型动作创建一个Widget 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

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

    int x, y;
    ...
    double d = static_cast<double>(x)/y; // x 除以y,使用浮点数除法
    // floating point division
    
    • 1
    • 2
    • 3
    • 4

      将int x转换成double肯定会产生代码,因为在大部分计算机体系结构中,int的底层表述不同于double的底层表述。这也许不会让你吃惊,但下面的例子可能需要你多多注意了:

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

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

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

      偏移量“有时候“是需要的。对象的布局方式和地址被计算的方式会随编译器的不同而不同。这意味着仅仅因为你了解一种平台上的布局和转型并不意味着在别的平台上也能如此工作。

    3、转型的误用

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

    class Window {                       // base class
    public:                                   
    	virtual void onResize() { ... }           // base onResize 实现代码
    	...                                                   
    };                                                   
    class SpecialWindow: public Window {       // derived class
    public:                                                       
    	virtual void onResize() {                             // derived onResize 实现代码
    	static_cast<Window>(*this).onResize();     // 将 *this 转为 Window,
    	...	
    	} 
    ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为什么上面的类型转换是错误的:

    • 我们在函数中将*this转换为基类类型,然后尝试调用onResize()虚函数,但是该转型动作产生的实际上是一个“this对象之基类成分”的一个副本,然后在这个副本上调用onResize函数

    • 所以当在副本上面调用onResize()函数对于this对象根本没有任何的影响,于是客户端程序以为调用了onResize()函数使对象本身改变了,但是实际上没有改变,只是改变了一个临时对象而已

      替代方案:就是在虚函数中显式的调用基类的函数。例如将上面派生类的虚函数更改为下面的形式就是对的了

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

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

    4、关于dynamic_cast的一些概述

      dynamic_cast主要用于:想要使用派生类的方法,但是此时只有一个基类的指针/引用,此时可以使用该类型转换将基类的指针转换为派生类指针。

    为什么不建议使用该转型:因为该转换会使代码执行速度的非常慢

    举个例子,至少有一种普通的实现在某种程度上是基于类名称的字符串比较。如果你正在一个4层深的单继承体系的对象上执行dynamic_cast,在这样一种实现(也就是上面说的普通实现)下每个dynamic_cast至多可能调用四次strcmp来比较类名称。一个层次更深的继承或者一个多继承可能开销会更大。这样实现是有原因的(它们必须支持动态链接(dynamic linking))。

    4.1 Dynamic_cast的两种替代方案

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

    • 第一,使用容器直接存储派生类对象指针(通常情况下使用智能指针,见条款13),这样就消除了通过基类接口来操纵这些对象的可能。

      举个例子,在我们的window/SpecialWindow继承体系中,只有SpecialWindows支持blink,不要像下面这样做:

    class Window { ... };
     
    class SpecialWindows: public Window {
    public:
        void blink();
        ...
    };
    int main()
    {
    	typedef
        std::vector<std::tr1::shared_ptr<Window> > VPW;
    	
        //遍历容器中的每个Window对象并且调用blink函数
        for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
            //使用dynamic_cast转型,get()函数是获取shared_ptr中的Window对象指针的意思
            if (SpecialWindows* psw = dynamic_cast<SpecialWindows*>(iter->get())) {// 不希望使用dynamic_cast
                psw->blink();
            }
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    替代方法① :直接使用容器存储派生类对象,不存储基类对象

    int main()
    {
    	typedef
        std::vector<std::tr1::shared_ptr<SpecialWindows> > VPW;
    	
        for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
            (*iter)->blink();
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    缺点:但是这种方法可能需要为每一种派生类都创建一个容器来存储

    替代②:当你在基类中想做一些事情,那么也就基类中同时声明一份,并且将函数设置为virtual的(但是基类中的虚函数什么都不做)

    class Window {
    public:
        virtual void blink() { //条款34告诉你缺省实现代码可能是个馊主意
            //什么都不做
        }
    };
     
    class SpecialWindows :public Window {
    public:
        virtual void blink() {
            //实现相关代码
        }
    };
     
    int main()
    {
        std::vector<std::tr1::shared_ptr<Window> > VPW;
    	
        for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
            (*iter)->blink();
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

      绝对必须避免的一件事情就是所谓的“连串dynamic_cast”,例如下面的代码,下面的代码运行又大又慢,而且基础不稳定,因为每次Window类继承体系一旦改变,所有的代码都需要再次更改

    class Window {};
    class SpecialWindows :public Window {};
    class SpecialWindows2 :public Window {};
    class SpecialWindows3 :public Window {};
    //...
     
    int main()
    {
        std::vector<std::tr1::shared_ptr<Window> > VPW;
     
        for (auto iter = VPW.begin(); iter != VPW.end(); ++iter)
        {
            if (SpecialWindows *psw1 = dynamic_cast<SpecialWindows*>(iter->get())) {
                //...
            }
            else if (SpecialWindows2 *psw2 = dynamic_cast<SpecialWindows2*>(iter->get())) {
                //...
            }
            else if (SpecialWindows3 *psw3 = dynamic_cast<SpecialWindows3*>(iter->get())) {
                //...
            }
            //...
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5、牢记

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

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

    • 宁可使用新式的转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    springboot和springcloud 和springcloud Alibaba的版本选择
    fatal: bad boolean config value ‘“false”‘ for ‘http.sslverify
    FreeRTOS中断与任务之间同步(Error:..\..\FreeRTOS\portable\RVDS\ARM_CM4F\port.c,422 )
    JFrame中有关于DefaultCloseOperation的使用及参数说明(含源码阅读)
    redis
    Linux设备使用阿里云盘终极方案
    基于STM32的森林火灾监控系统设计
    LeetCode每日一题——2558. Take Gifts From the Richest Pile
    称重系统为了做到无人,电气设备需要什么样控制要求
    Nacos源码系列—订阅机制的前因后果(上)
  • 原文地址:https://blog.csdn.net/CltCj/article/details/128197060