完美转发

  • 作者: 雪山肥鱼
  • 时间: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 
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 
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 
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 
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(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 
void funcMiddle_Temp(F f, T1 &&t1, T2 &&t2) {
    f(
        //forward 将t1 转换为 T1, 如果T1 为int,则 t1 被转化为右值
        //std::forward(t1),//T1: int, t1:int && 可行
        //std::forward(t1),//T1: int, t1:int && 可行
        std::forward(t1),//T1: int, t1:int &&
        std::forward(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 要转换成的类型。

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 
void TestF(T && t) {
    printfInfo(std::forward(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(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 
    Human(T&& tmpname) :m_sname(std::forward(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 的

  • 增加拷贝构造函数
class Human {
public:
    template 
    Human(T&& tmpname) :m_sname(std::forward(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 
    Human(T&& tmpname) :m_sname(std::forward(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 
    Human(T&& tmpname) :m_sname(std::forward(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 
void funcMiddle_Temp(F f, T1 && t1, T2 &&t2) {
    f(
        std::forward(t1),
        std::forward(t2)
    )
}
*/

//目标函数不变
void funclast(int v1, int &v2)
{
    ++v2;
    cout << v1 + v2 << endl;
}

//支持任意数量,类型参数的完美转发
template 
void funcMiddle_Temp(F f, T &&... t) {
    f(std::forward(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 
auto funcMiddle_Temp(F f, T &&... t) -> decltype(f(std::forward(t)...)) {
    return f(std::forward(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)...);
}

完美转发失败情形一例

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

#pragma warning(disable:4996)
void funcLast4(char *p) {
   if (p != NULL) {
       strncpy(p, "abc", 3);
   }
}

template 
void funcMiddle_Temp(F f, T &&... t) {
   f(std::forward(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

你可能感兴趣的:(完美转发)