美文网首页
完美转发

完美转发

作者: 404Not_Found | 来源:发表于2022-02-11 23:52 被阅读0次
    • 作者: 雪山肥鱼
    • 时间:20220211 23:58
    • 目的: 模板中完美转发
    # 完美转发的概念和步骤演绎
    # std::forward
    # 普通参数的完美转发
    # 在构造函数模板中使用完美转发范例
    # 在可变参模板中使用完美转发
      ## 常规的在可变参模板使用完美转发
      ## 将目标函数中返回指通过转发函数返回按给调用者
    # 完美转发失败情形一例
    

    完美转发

    • 直接调用:
      funcLast();
    • 转发:
      通过funcMiddle(); 去简介调用 funcLast。当然funcLast 的参数 需要 funcMiddle 进行传递。
    • 完美转发
      funcLast(),的形参 有const,左值,右值的属性,经过funcMiddle 进行转发到funcLast,会丢失这些属性。说明这样的转发并不完美。
      完美转发:转发过程中,实参属性不丢失,就是完美转发。

    表面可以正常工作的转发:

    //表面可以正常工作
    void funcLast(int v1, int v2) {
        ++v2;
        cout << v1 + v2 << endl;
    }
    
    //增加函数模板,跳板函数示例
    //把受到的参数以及这些参数相对应的类型不变的转发给其他函数。(完美转发)
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 t1, T2 t2) {//f 函数指针类型 void(*)(int, int)
        f(t1, t2);
    }
    
    int main(int argc, char ** argv) {
        int j = 70;
        funcMiddle_Temp(funcLast, 20, j);//result 91, j 70
        return 0;
    }
    

    引出转发, 将funcLast v2 改成引用

    void funcLast(int v1, int &v2) {
        ++v2;
        cout << v1 + v2 << endl;
    }
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 t1, T2 t2) {//f 函数指针类型 void(*)(int, int)
        f(t1, t2);
    }
    
    int main(int argc, char ** argv) {
        /*
        直接调用 没有问题,正确
        int i = 50;
        funcLast(41, i);//92, i变为51
        return 0;
        */
    
        int j = 70;
        funcMiddle_Temp(funcLast, 20, j);//91, j 要变成 71,但实际上j的值,仍然为70
    
        return 0;
    }
    

    在调用funcMiddle_temp 的始化,我们希望j的值最后发生变化,但是由于转发的缺陷,造成j的值并没有发生变换。
    j 进入 funcMiddle_temp 被推断成了int,而不是 int&,所以并没有修改到值。其实也是绑定的 临时变量t2.

    引入万能引用,实参的所有信息,都会传递到万能引用中去,从而编译器推导出来最终的形参类型。

    void funcLast(int v1, int &v2) {
        ++v2;
        cout << v1 + v2 << endl;
    }
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 &&t1, T2 &&t2) {//单纯的使用 T2 & t2,只能保证const属性的保留,但破坏了 左值性,右值性,这里留一个疑问。
        f(t1, t2);
    }
    
    int main(int argc, char ** argv) {
        /*
        直接调用 没有问题,正确
        int i = 50;
        funcLast(41, i);//92, i变为51
        return 0;
        */
    
        int j = 70;
        funcMiddle_Temp(funcLast, 20, j);//91, j 要变成 71,但实际上j的值,仍然为70
        cout << j << endl;
    
        return 0;
    

    这里注意注释内容:单纯的使用 T2 & t2,只能保证const属性的保留,但破坏了 左值性,右值性,这里留一个疑问。

    • 20 进到 间接函数
      T1:int, t1: int&&
      T2: int, t2 :int &(见万能引用一节)

    此时 j 的值由70 变成了 71.

    引出新问题,如果funcLast形参 类型为 int &&.

    void funcLast2(int &&v1, int &v2) {
        cout << v1 << endl;
        cout << v2 << endl;
    }
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 &&t1, T2 &&t2) {
        f(t1, t2);
    }
    
    int main(int argc, char ** argv) {
    
        int j = 70;
        funcMiddle_Temp(funcLast2, 20, j);
        cout << j << endl;
    
        return 0;
    }
    

    v1 的类型 变为 右值时,编译错误。

     error C2664: “void (int &&,int &)”: 无法将参数 1 从“T1”转换为“int &&”
    1>        with
    1>        [
    1>            T1=int
    1>        ]
    1>c:\users\liush\source\repos\wanmeizhuanfa\main.cpp(33): note: 无法将左值绑定到右值引用
    1>c:\users\liush\source\repos\wanmeizhuanfa\main.cpp(39): note: 参见对正在编译的函数 模板 实例化“void funcMiddle_Temp<void(__cdecl *)(int &&,int &),int,int&>(F,T1 &&,T2)”的引用
    1>        with
    1>        [
    1>            F=void (__cdecl *)(int &&,int &),
    1>            T1=int,
    1>            T2=int &
    1>        ]
    1>已完成生成项目“wanmeizhuanfa.vcxproj”的操作 - 失败。
    

    分析:
    20 -> t1(int &&), t1的类型是 右值引用类型,int&&,但t1 本身是左值,

    int && abc = 1; //abc 类型是 右值
    abc = 145;//但是 abc 本身 是左值
    

    所以 出现问题,在funcLast2中用右值类型 去接 左值,形参:int&& v1,只接受右值(20, 10这种)!

    如果能把跳板函数中的 t1 类型扭成 右值类型,则看成完美转发

    完美转发: 就是让程序员可以书写接受任意实参的函数模板,并将其转发到目标函数(funcLast2),其中包括类型相同,参数的左值右值性和const属性等

    引出 std::forward.

    std::forward

    c++11 专门为转发而存在的函数。
    这个函数要么返回一个左值,要么返回一个右值。
    std::forward 只作用在万能引用类型上。
    理解(类型转换符):

    1. 实参原来是个左值,到了跳板函数中形参中 ,还是左值 t2. forward能够转转化回原来该实参的左值或者右值性。也就是 forwad后,依旧是左值。
    2. 实参原来是右值20, 到了跳板函数形参中,变成了左值t1. forword能够转化回原来该实参的左右值性。所以fowrd后,t1被转化为右值。forward有强制把左值转换成右值的能力。
      所以forward,只对原来是右值,经过跳板变成左值,这种情况有用。
    void funcLast2(int &&v1, int &v2) {
        cout << v1 << endl;
        cout << v2 << endl;
    }
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 &&t1, T2 &&t2) {
        f(
            //forward 将t1 转换为 T1, 如果T1 为int,则 t1 被转化为右值
            //std::forward<int>(t1),//T1: int, t1:int && 可行
            //std::forward<int&&>(t1),//T1: int, t1:int && 可行
            std::forward<T1>(t1),//T1: int, t1:int &&
            std::forward<T2>(t2)// T2: int& t2: int& 转为左值
        );
    }
    
    int main(int argc, char ** argv) {
        /*
        直接调用没问题
        int j = 70;
        funcLast2(20, j)
        */
    
        int j = 70;
        funcMiddle_Temp(funcLast2, 20, j);
        cout << j << endl;
    
        return 0;
    }
    

    主要是看 std::forward<T1>(t1),看 <>中的就是 t1 要转换成的类型。

    forward的能力:保持原始实参的左值性或右值性。

    void printfInfo(int &t) {
        cout << "printfInfo() 参数类型为 左值引用" << endl;
    }
    
    void printfInfo(int &&t) {
        cout << "printfInfo() 参数类型为 右值引用" << endl;
    }
    
    void printfInfo(const int &t) {
        cout << "printfInfo() 参数类型为 const 左值引用" << endl;
    }
    
    template <typename T>
    void TestF(T && t) {
        printfInfo(std::forward<T>(t));
    }
    
    
    int main(int argc, char ** argv) {
        TestF(1);//右值
    
        int i = 5;
        TestF(i);//左值
    
        TestF(std::move(i));//左值转右值,右值
    
        const int j = 8;
        TestF(j);//const 左值
    
        TestF(int(12));//int(12) 临时对象,是个右值
    
        int&& tmpvalue = 16;
        TestF(tmpvalue);//左值
    
        return 0;
    }
    

    总结:完美转发,比较好的解决了跳板函数到转发函数参数传值过程中的左右值问题。

    普通参数的完美转发

    函数返回左值与右值:

    int g_a = 10;
    int & getData() {
      return g_a;//返回的是左值
    }
    getData() = 0;//可以这么玩
    
    int getData() {
      return 3;//返回的是右值
    }
    getData() = 6;//不能这么玩
    
    int getdata() {
      return g_a;//返回的是右值
    }
    getData() = 6;//不能这么玩
    
    int getData() {
        return 3;//返回的是右值
    }
    
    void funcLast3(int v1) {
        cout << "v1 = " << v1 << endl;
    }
    
    void funcMiddle_Temp2() {
        auto && result = getData();//getData 返回的是右值,auto:int,result :int &&(右值)
        /*
        验证:
        getData() = 6;//右值
        */
        //对 result做各种运算,result本身是左值。
        funcLast3(
            std::forward<decltype(result)>(result)//还原回右值
        );
    }
    
    int main(int argc, char ** argv) {
        /*
            直接调用    
        funcLast3(getData());
    
        */
        funcMiddle_Temp2();
        return 0;
    }
    

    在构造函数模板中使用完美转发

    简单复习:

    class Human {
    public:
        Human(const string & tmpname):m_sname(tmpname) {
            cout << "Human(const string & tmpname) 执行" << endl;
        }
    
    private:
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        //调用2次构造函数
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));//临时变量"Lisi" const char[5] -> string
        return 0;
    }
    
    图片.png

    增加一个 右值引用的构造函数

    class Human {
    public:
        Human(const string & tmpname):m_sname(tmpname) {
            cout << "Human(const string & tmpname) 执行" << endl;
        }
    
        Human(string && tmpname) :m_sname(tmpname) {
            cout << "Human(string && tmpname) 执行" << endl;
        }
    
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        //调用2次构造函数
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));//临时变量"Lisi" const char[5] -> string
        return 0;
    }
    
    图片.png

    临时的东西 ,永远是右值。

    再次修改

    class Human {
    public:
        Human(const string & tmpname):m_sname(tmpname) {
            cout << "Human(const string & tmpname) 执行" << endl;
        }
    //修改成 右值
        Human(string && tmpname) :m_sname(std::move(tmpname)) {
            cout << "Human(string && tmpname) 执行" << endl;
        }
    
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        //调用2次构造函数
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));//临时变量"Lisi" const char[5] -> string
        return 0;
    }
    

    将右值字符串给了 m_sname 会导致什么?
    会调用 string 的 移动构造函数。即将 tmpname 清空,值转移给m_sname.

    注意,转移的动作是 string 的移动构造做的,与std::move()无关。

    可以用万能引用将两个构造函数和二为1.

    class Human {
    public:
        template <typename T>
        Human(T&& tmpname) :m_sname(std::forward<T>(tmpname)) {
            cout << "Human(T&& tmpname) 执行" << endl;
        }
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));
        return 0;
    }
    

    不要忘记 std::forward 的 <T>

    • 增加拷贝构造函数
    class Human {
    public:
        template <typename T>
        Human(T&& tmpname) :m_sname(std::forward<T>(tmpname)) {
            cout << "Human(T&& tmpname) 执行" << endl;
        }
    
        Human(const Human & th) : m_sname(th.m_sname) {
            cout << "Human(const Human &th) copy构造函数执行" << endl;
        }
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));
    
        Human myhuman3(myhuman1);//编译错误,编译器无法调用拷贝构造,原因是构造函数模板的存在,stirng类型和 myhuman1类型,肯定无法兼容
        return 0;
    }
    

    译错误,编译器无法调用拷贝构造,原因是构造函数模板的存在,stirng类型和 myhuman1类型,肯定无法兼容

    后续再解决这个问题。即 std::enable_if 的引入,后续再说。

    • 增加移动构造函数
    class Human {
    public:
        template <typename T>
        Human(T&& tmpname) :m_sname(std::forward<T>(tmpname)) {
            cout << "Human(T&& tmpname) 执行" << endl;
        }
    
        Human(const Human & th) : m_sname(th.m_sname) {
            cout << "Human(const Human &th) copy构造函数执行" << endl;
        }
    
        //移动构造函数
        Human(Human &&th) :m_sname(std::move(th.m_sname)) {
            cout << "Human(Human &&th) 移动构造被执行" << endl;
        }
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));
    
        //Human myhuman3(myhuman1);//编译错误,编译器无法调用拷贝构造,原因是构造函数模板的存在,stirng类型和 myhuman1类型,肯定无法兼容
    
        Human myhuman4(std::move(myhuman1));//没有问题
        return 0;
    }
    
    • 再次修改,增加const
    class Human {
    public:
        template <typename T>
        Human(T&& tmpname) :m_sname(std::forward<T>(tmpname)) {
            cout << "Human(T&& tmpname) 执行" << endl;
        }
    
        Human(const Human & th) : m_sname(th.m_sname) {
            cout << "Human(const Human &th) copy构造函数执行" << endl;
        }
    
        //移动构造函数
        Human(Human &&th) :m_sname(std::move(th.m_sname)) {
            cout << "Human(Human &&th) 移动构造被执行" << endl;
        }
    private:
    
        string m_sname;
    };
    
    int main(int argc, char **argv) {
        string sname = "ZhangSan";
        Human myhuman1(sname);
        Human myhuman2(string("Lisi"));
    
        //Human myhuman3(myhuman1);//编译错误,编译器无法调用拷贝构造,原因是构造函数模板的存在,stirng类型和 myhuman1类型,肯定无法兼容
    
        Human myhuman4(std::move(myhuman1));
        const Human myhuman5(string("Wangwu"));
        Human myhuman6(myhuman5);
        return 0;
    }
    

    拷贝构造函数被正常执行

    在可变参模板中使用完美转发

    常规的在可变参模板使用完美转发

    /*
    void funclast(int v1, int &v2)
    {
        ++v2;
        cout << v1 + v2 << endl;
    }
    
    template <typename F, typename T1, typename T2>
    void funcMiddle_Temp(F f, T1 && t1, T2 &&t2) {
        f(
            std::forward<T1>(t1),
            std::forward<T2>(t2)
        )
    }
    */
    
    //目标函数不变
    void funclast(int v1, int &v2)
    {
        ++v2;
        cout << v1 + v2 << endl;
    }
    
    //支持任意数量,类型参数的完美转发
    template <typename F, typename...T>
    void funcMiddle_Temp(F f, T &&... t) {
        f(std::forward<T>(t)...);
    }
    
    int main(int argc, char **argv) {
        int j = 80;
        funcMiddle_Temp(funclast, 20, j);
        cout << "j = " << j << endl;
        return 0;
    }
    

    result: 101, j = 81

    将目标函数中返回指通过转发函数返回按给调用者

    结合 auto + decltype构成返回值类型后置,

    int funclast(int v1, int &v2)
    {
        ++v2;
        cout << v1 + v2 << endl;
        return v1 + v2;
    }
    
    template <typename F, typename...T>
    auto funcMiddle_Temp(F f, T &&... t) -> decltype(f(std::forward<T>(t)...)) {
        return f(std::forward<T>(t)...);
    }
    
    int main(int argc, char **argv) {
        int j = 80;
        int k = funcMiddle_Temp(funclast, 20, j);
        cout << "k = " << k << endl;
        return 0;
    }
    

    对这个写法有印象即可。

    更清晰,更安全的写法:

    decltype(auto) funcMiddle_Temp(F f, T &&... t) -> {
        return f(std::forward<T>(t)...);
    }
    

    完美转发失败情形一例

    使用 null 或者 0 作为参数传递时导致完美转发失败情形

    #pragma warning(disable:4996)
    void funcLast4(char *p) {
       if (p != NULL) {
           strncpy(p, "abc", 3);
       }
    }
    
    template <typename F, typename...T>
    void funcMiddle_Temp(F f, T &&... t) {
       f(std::forward<T>(t)...);
    }
    int main(int argc, char **argv) {
       char *p = new char[100];
       memset(p, 0, 100);
    
       //funcMiddle_Temp(funcLast4, NULL);//编译失败,NULL无法转发成功
       funcMiddle_Temp(funcLast4, nullptr);//失败原因,NULL , 0 并不被看成指针类型,整形不能被作为空指针进行完美转发
       return 0;
    }s
    

    失败原因,NULL , 0 并不被看成指针类型,整形不能被作为空指针进行完美转发.

    对于指针,尽量不要用NULL,最好用 nullptr

    相关文章

      网友评论

          本文标题:完美转发

          本文链接:https://www.haomeiwen.com/subject/leftlrtx.html