- 作者: 雪山肥鱼
- 时间: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 只作用在万能引用类型上。
理解(类型转换符):
- 实参原来是个左值,到了跳板函数中形参中 ,还是左值 t2. forward能够转转化回原来该实参的左值或者右值性。也就是 forwad后,依旧是左值。
- 实参原来是右值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
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;
}
增加一个 右值引用的构造函数
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;
}
临时的东西 ,永远是右值。
再次修改
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