• 第15课 完美转发(std::forward)


    一、理解引用折叠 

    (一)引用折叠

      1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意两个&之间有空格)这种直接定义引用的引用是不合法的,但是编译器在通过类型别名或模板参数推导等语境中,会间接定义出“引用的引用”,这时引用会形成“折叠”。

      2. 引用折叠会发生在模板实例化、auto类型推导、创建和运用typedef和别名声明、以及decltype语境中

    (二)引用折叠规则

      1. 两条规则

        (1)所有右值引用折叠到右值引用上仍然是一个右值引用。如X&& &&折叠为X&&。

        (2)所有的其他引用类型之间的折叠都将变成左值引用。如X& &, X& &&, X&& &折叠为X&。可见左值引用会传染,沾上一个左值引用就变左值引用了根本原因:在一处声明为左值,就说明该对象为持久对象,编译器就必须保证此对象可靠(左值)

      2. 利用引用折叠进行万能引用初始化类型推导

        (1)当万能引用(T&& param)绑定到左值时,由于万能引用也是一个引用,而左值只能绑定到左值引用。因此,T会被推导为T&类型。从而param的类型为T& &&,引用折叠后的类型为T&。

        (2)当万能引用(T&& param)绑定到右值时,同理,右值只能绑定到右值引用上,故T会被推导为T类型。从而param的类型就是T&&(右值引用)。

    【编程实验】引用折叠

    #include <iostream>
    
    using namespace std;
    
    class Widget{};
    
    template<typename T>
    void func(T&& param){}
    
    //Widget工厂函数
    Widget widgetFactory() 
    {
        return Widget();
    }
    
    //类型别名
    template<typename T>
    class Foo
    {
    public:
        typedef T&& RvalueRefToT;
    };
    
    int main()
    {
        int x = 0;
        int& rx = x;
        //auto& & r = x; //error,声明“引用的引用”是非法的!
    
        //1. 引用折叠发生的语境1——模板实例化
        Widget w1;
        func(w1); //w1为左值,T被推导为Widget&。代入得void func(Widget& && param);
                  //引用折叠后得void func(Widget& param)
    
        func(widgetFactory()); //传入右值,T被推导为Widget,代入得void func(Widget&& param)
                               //注意这里没有发生引用的折叠。
    
        //2. 引用折叠发生的语境2——auto类型推导
        auto&& w2 = w1; //w1为左值auto被推导为Widget&,代入得Widget& && w2,折叠后为Widget& w2
        auto&& w3 = widgetFactory(); //函数返回Widget,为右值,auto被推导为Widget,代入得Widget w3
    
        //3. 引用折叠发生的语境3——tyedef和using
        Foo<int&> f1;  //T被推导为int&,代入得typedef int& && RvalueRefToT;折叠后为typedef int& RvalueRefToT
    
        //4. 引用折叠发生的语境3——decltype
        decltype(x)&& var1 = 10;  //由于x为int类型,代入得int&& rx。
        decltype(rx) && var2 = x; //由于rx为int&类型,代入得int& && var2,折叠后得int& var2
    
        return 0;
    }
    引用折叠示例代码

    二、完美转发

    (一)std::forward原型

    template<typename T>
    T&& forward(typename remove_reference<T>::type& param)
    {
        return static_cast<T&&>(param); //可能会发生引用折叠!
    }

    (二)分析std::forward<T>实现条件转发的原理(以转发Widget类对象为例

     

      1. 当传递给func函数的实参类型左值Widget时,T被推导为Widget&类别。然后forward会实例化为std::forward<Widget&>,并返回Widget&(左值引用,根据定义是个左值!

      2. 而当传递给func函数的实参类型右值Widget时,T被推导为Widget。然后forward被实例化为std::forward<Widget>,并返回Widget&&(注意,匿名的右值引用是个右值!)

      3. 可见,std::forward会根据传递给func函数实参(注意,不是形参)的左/右值类型进行转发当传给func函数左值实参时,forward返回左值引用,并将该左值转发给process。而当传入func的实参为右值时,forward返回右值引用,并将该右值转发给process函数。

    【编程实验】不完美转发和完美转发

    #include <iostream>
    using namespace std;
    
    void print(const int& t)  //左值版本
    {
        cout <<"void print(const int& t)" << endl;
    }
    
    void print(int&& t)     //右值版本
    {
        cout << "void print(int&& t)" << endl;
    }
    
    template<typename T>
    void testForward(T&& param)
    {
        //不完美转发
        print(param);            //param为形参,是左值。调用void print(const int& t)
        print(std::move(param)); //转为右值。调用void print(int&& t)
    
        //完美转发
        print(std::forward<T>(param)); //只有这里才会根据传入param的实参类型的左右值进转发
    }
    
    int main()
    {
        cout <<"-------------testForward(1)-------------" <<endl;
        testForward(1);    //传入右值
    
        cout <<"-------------testForward(x)-------------" << endl;
        int x = 0;
        testForward(x);    //传入左值
    
        return 0;
    }
    /*输出结果
    -------------testForward(1)-------------
    void print(const int& t)
    void print(int&& t)
    void print(int&& t)       //完美转发,这里转入的1为右值,调用右值版本的print
    -------------testForward(x)-------------
    void print(const int& t)
    void print(int&& t)
    void print(const int& t) //完美转发,这里转入的x为左值,调用左值版本的print
    */
    不完美转发和完美转发示例代码

    三、std::move和std::forward

    (一)两者比较

      1. move和forward都是仅仅执行强制类型转换的函数。std::move无条件地将实参强制转换成右值。而std::forward则仅在某个特定条件满足时(传入func的实参是右值时)才执行强制转换

      2. std::move并不进行任何移动,std::forward也不进行任何转发。这两者在运行期都无所作为。它们不会生成任何可执行代码,连一个字节都不会生成。

    (二)使用时机

      1. 针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward

      2. 在按值返回的函数中,如果返回的是一个绑定到右值引用或万能引用的对象时,可以实施std::move或std::forward。因为如果原始对象是一个右值,它的值就应当被移动到返回值上,而如果是左值,就必须通过复制构造出副本作为返回值。

    (三)返回值优化(RVO)

      1.两个前提条件

        (1)局部对象类型函数返回值类型相同

        (2)返回的就是局部对象本身(含局部对象或作为return 语句中的临时对象等)

      2. 注意事项

        (1)在RVO的前提条件被满足时,要么避免复制要么会自动地用std::move隐式实施于返回值

        (2)按值传递的函数形参,把它们作为函数返回值时,情况与返回值优化类似。编译器这里会选择第2种处理方案,即返回时将形参转为右值处理

        (3)如果局部变量有资格进行RVO优化,就不要把std::move或std::forward用在这些局部变量中。因为这可能会让返回值丧失优化的机会。

    【编程实验】RVO优化和std::move、std::forward

    #include <iostream>
    #include <memory>
    using namespace std;
    
    //1. 针对右值引用实施std::move,针对万能引用实施std::forward
    class Data{};
    
    class Widget
    {
        std::string name;
        std::shared_ptr<Data> ptr;
    public:
        Widget() { cout <<"Widget()"<<endl; };
    
        //复制构造函数
        Widget(const Widget& w):name(w.name), ptr(w.ptr)
        {
            cout <<"Widget(const Widget& w)" << endl;
        }
        //针对右值引用使用std::move
        Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr))
        {
            cout << "Widget(Widget&& rhs)" << endl;
        }
    
        //针对万能引用使用std::forward。
        //注意,这里使用万能引用来替代两个重载版本:void setName(const string&)和void setName(string&&)
        //好处就是当使用字符串字面量时,万能引用版本的效率更高。如w.setName("SantaClaus"),此时字符串会被
        //推导为const char(&)[11]类型,然后直接转给setName函数(可以避免先通过字量面构造临时string对象)。
        //并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高。
        template<typename T>
        void setName(T&& newName)
        {
            if (newName != name) { //第1次使用newName
                name = std::forward<T>(newName); //针对万能引用的最后一次使用实施forward
            }
        }
    };
    
    //2. 按值返回函数
    //2.1 按值返回的是一个绑定到右值引用的对象
    class Complex 
    {
        double x;
        double y;
    public:
        Complex(double x =0, double y=0):x(x),y(y){}
        Complex& operator+=(const Complex& rhs) 
        {
            x += rhs.x;
            y += rhs.y;
            return *this;
        }
    };
    
    Complex operator+(Complex&& lhs, const Complex& rhs) //重载全局operator+
    {
        lhs += rhs;
        return std::move(lhs); //由于lhs绑定到一个右值引用,这里可以移动到返回值上。
    }
    
    //2.2 按值返回一个绑定到万能引用的对象
    template<typename T>
    auto test(T&& t)
    {
        return std::forward<T>(t); //由于t是一个万能引用对象。按值返回时实施std::forward
                                   //如果原对象一是个右值,则被移动到返回值上。如果原对象
                                   //是个左值,则会被拷贝到返回值上。
    }
    
    //3. RVO优化
    //3.1 返回局部对象
    Widget makeWidget()
    {
        Widget w;
    
        return w;  //返回局部对象,满足RVO优化两个条件。为避免复制,会直接在返回值内存上创建w对象。
                   //但如果改成return std::move(w)时,由于返回值类型不同(Widget右值引用,另一个是Widget)
                   //会剥夺RVO优化的机会,就会先创建w局部对象,再移动给返回值,无形中增加一个移动操作。
                   //对于这种满足RVO条件的,当某些情况下无法避免复制的(如多路返回),编译器仍会默认地对
                   //将w转为右值,即return std::move(w),而无须用户显式std::move!!!
    }
    
    //3.2 按值形参作为返回值
    Widget makeWidget(Widget w) //注意,形参w是按值传参的。
    {
        //...
    
        return w; //这里虽然不满足RVO条件(w是形参,不是函数内的局部对象),但仍然会被编译器优化。
                  //这里会默认地转换为右值,即return std::move(w)
    }
    
    int main()
    {
        cout <<"1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl;
        Widget w;
        w.setName("SantaClaus");
    
        cout << "2. 按值返回时" << endl;
        auto t1 = test(w); 
        auto t2 = test(std::move(w));
    
        cout << "3. RVO优化" << endl;
        Widget w1 = makeWidget();   //按值返回局部对象(RVO)
        Widget w2 = makeWidget(w1); //按值返回按值形参对象
    
        return 0;
    }
    /*输出结果
    1. 针对右值引用实施std::move,针对万能引用实施std::forward
    Widget()
    2. 按值返回时
    Widget(const Widget& w)
    Widget(Widget&& rhs)
    3. RVO优化
    Widget()
    Widget(Widget&& rhs)
    Widget(const Widget& w)
    Widget(Widget&& rhs)
    */

    四、完美转发失败的情形

    (一)完美转发失败

      1. 完美转发不仅转发对象,还转发其类型、左右值特征以及是否带有const或volation等修饰词。而完美转发的失败,主要源于模板类型推导失败或推导的结果是错误的类型。

      2. 实例说明:假设转发的目标函数f,而转发函数为fwd(天然就应该是泛型)。函数如下:

    template<typename… Ts>
    void fwd(Ts&&… params)
    {
         f(std::forward<Ts>(params)…);
    }
    
    f(expression);    //如果本语句执行了某操作
    fwd(expression);  //而用同一实参调用fwd则会执行不同操作,则称完美转发失败。

    (二)五种完美转发失败的情形

      1. 使用大括号初始化列表时

      (1)失败原因分析:由于转发函数是个模板函数,而在模板类型推导中,大括号初始不能自动被推导为std::initializer_list<T>

      (2)解决方案:先用auto声明一个局部变量,再将该局部变量传递给转发函数。

      2. 0和NULL用作空指针时

      (1)失败原因分析:0或NULL以空指针之名传递给模板时,类型推导的结果是整型而不是所希望的指针类型。

      (2)解决方案:传递nullptr,而非0或NULL。

      3. 仅声明static const 整型成员变量,而无其定义时。

      (1)失败原因分析:C++中常量一般是进入符号表的,只有对其取地址时才会实际分配内存。调用f函数时,其实参是直接从符号表中取值,此时不会发生问题。但当调用fwd时由于其形参是万能引用,而引用本质上是一个可解引用的指针。因此当传入fwd时会要求准备某块内存以供解引用出该变量出来但因其未定义,也就没有实际的内存空间, 编译时可能失败(取决于编译器和链接器的实现)。

      (2)解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值

      4. 使用重载函数名或模板函数名

      (1)失败原因分析:由于fwd是个模板函数,其形参没有任何关于类型的信息。当传入重载函数名或模板函数(代表许许多多的函数)时,就会导致fwd的形参不知绑定到哪个函数上。

      (2)解决方案:在调用fwd调用时手动为形参指定类型信息。

      5. 转发位域时

      (1)失败原因分析:位域是由机器字的若干任意部分组成的(如32位int的第3至5个比特),但这样的实体是无法直接取地址的。而fwd的形参是个引用,本质上就是指针,所以也没有办法创建指向任意比特的指针

      (2)解决方案:制作位域值的副本,并以该副本来调用转发函数。

    【编程实验】完美转发失败的情形及解决方案

    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    //1. 大括号初始化列表
    void f(const std::vector<int>& v)
    {
        cout << "void f(const std::vector<int> & v)" << endl;
    }
    
    //2. 0或NULL用作空指针时
    void f(int x)
    {
        cout << "void f(int x)" << endl;
    }
    
    
    //3. 仅声明static const的整型成员变量而无定义
    class Widget
    {
    public:
        static const  std::size_t MinVals = 28; //仅声明,无定义(因为静态变量需在类外定义!)
    };
    
    //const std::size_t Widget::MinVals; //在类外定义,无须也不能重复指定初始值。
    
    //4. 使用重载函数名或模板函数名
    int f(int(*pf)(int))
    {
        cout <<"int f(int(*pf)(int))" << endl;
        return 0;
    }
    
    int processVal(int value) { return 0; }
    int processVal(int value, int priority) { return 0; }
    
    //5.位域
    struct IPv4Header
    {
        std::uint32_t version : 4,
                      IHL : 4,
                      DSCP : 6,
                      ECN : 2,
                      totalLength : 16;
        //...
    };
    
    template<typename T>
    T workOnVal(T param)  //函数模板,代表许许多多的函数。
    {
        return param;
    }
    
    //用于测试的转发函数
    template<typename ...Ts>
    void fwd(Ts&& ... param)  //转发函数
    {
        f(std::forward<Ts>(param)...);  //目标函数
    }
    
    int main()
    {
        cout <<"-------------------1. 大括号初始化列表---------------------" << endl;    
        //1.1 用同一实参分别调用f和fwd函数
        f({ 1, 2, 3 });  //{1, 2, 3}会被隐式转换为std::vector<int>
        //fwd({ 1, 2, 3 }); //编译失败。由于fwd是个函数模板,而模板推导时{}不能自动被推导为std:;initializer_list<T>
        //1.2 解决方案
        auto il = { 1,2,3 };
        fwd(il);
    
        cout << "-------------------2. 0或NULL用作空指针-------------------" << endl;
        //2.1 用同一实参分别调用f和fwd函数
        f(NULL);   //调用void f(int)函数,
        fwd(NULL); //NULL被推导为int,仍调用void f(int)函数
        //2.2 解决方案:使用nullptr
        f(nullptr);  //匹配int f(int(*pf)(int))
        fwd(nullptr);
    
        cout << "-------3. 仅声明static const的整型成员变量而无定义--------" << endl;
        //3.1 用同一实参分别调用f和fwd函数
        f(Widget::MinVals);   //调用void f(int)函数。实参从符号表中取得,编译成功!
        fwd(Widget::MinVals); //fwd的形参是引用,而引用的本质是指针,但fwd使用到该实参时需要解引用
                              //这里会因没有为MinVals分配内存而出现编译失败(取决于编译器和链接器)
        //3.2 解决方案:在类外定义该变量
    
        cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl;
        //4.1 用同一实参分别调用f和fwd函数
        f(processVal);   //ok,由于f形参为int(*pf)(int),带有类型信息,会匹配int processVal(int value)
        //fwd(processVal); //error,fwd的形参不带任何类型信息,不知该匹配哪个processVals重载函数。
        //fwd(workOnVal);  //error,workOnVal是个函数模板,代表许许多多的函数。这里不知绑定到哪个函数
        //4.2 解决方案:手动指定类型信息
        using ProcessFuncType = int(*)(int);
        ProcessFuncType processValPtr = processVal;
        fwd(processValPtr);
        fwd(static_cast<ProcessFuncType>(workOnVal));   //调用int f(int(*pf)(int))
    
        cout << "----------------------5. 转发位域时---------------------" << endl;
        //5.1 用同一实参分别调用f和fwd函数
        IPv4Header ip = {};
        f(ip.totalLength);  //调用void f(int)
        //fwd(ip.totalLength); //error,fwd形参是引用,由于位域是比特位组成。无法创建比特位的引用!
        //解决方案:创建位域的副本,并传给fwd
        auto length = static_cast<std::uint16_t>(ip.totalLength);
        fwd(length);
        
        return 0;
    }
    /*输出结果
    -------------------1. 大括号初始化列表---------------------
    void f(const std::vector<int> & v)
    void f(const std::vector<int> & v)
    -------------------2. 0或NULL用作空指针-------------------
    void f(int x)
    void f(int x)
    int f(int(*pf)(int))
    int f(int(*pf)(int))
    -------3. 仅声明static const的整型成员变量而无定义--------
    void f(int x)
    void f(int x)
    -------------4. 使用重载函数名或模板函数名---------------
    int f(int(*pf)(int))
    int f(int(*pf)(int))
    int f(int(*pf)(int))
    ----------------------5. 转发位域时---------------------
    void f(int x)
    void f(int x)
    */
  • 相关阅读:
    OpenCV学习:图片的读取、展示
    Windows下使用Anaconda配置opencv和tensorflow环境
    二叉树的链式存储的实现以及对二叉树的各种操作
    数据结构——树笔记1
    Python学习笔记整理总结【Django】Ajax
    Python学习笔记整理总结【Django】:中间件、CSRF、缓存
    Python学习笔记整理总结【Django】:模板语言、分页、Cookie、Session
    Python学习笔记整理总结【Django】:Model操作(一)
    Python学习笔记整理总结【Django】【MVC/MTV/路由分配系统(URL)/视图函数 (views)/表单交互】
    数据结构与算法(C/C++版)【树与二叉树】
  • 原文地址:https://www.cnblogs.com/5iedu/p/11324772.html
Copyright © 2020-2023  润新知