我们知道,C++中有左值引用和右值引用,首先我们要知道什么是左值什么是右值。
左值:表达式结束后依然存在的持久对象。左值可以出现在赋值语句的左边或右边。例如,变量和函数返回的引用都是左值。左值通常有持久的地址,可以被取地址操作符 &
获取地址。
右值:通常是临时的,不能有多个引用,它们不指向持久存储。右值可以出现在赋值语句的右边,但不能出现在左边。字面量(如 42
或 "hello"
)和将亡值(函数返回的非引用类型)都是右值。右值通常用于描述临时对象或字面值,它们在使用后很快就会被销毁。
那么,在将左值引用和右值引用传入函数的时候,我们有时候需要保存它的左值属性或者右值属性,这时就需要分别写出左值引用和右值引用函数重载。我们也可能需要将不同类型的参数传入函数,这时候就需要用到函数模板。
函数重载如下:
void func(int &n){
cout << "左值=" << n << endl;
}
void func(int &&m){
cout << "右值=" << m << endl;
}
函数模板如下:
//第一种函数模板
template<class T>
void func(T &n){
cout << "左值=" << n << endl;
}
template<class T>
void func(T &&m){
cout << "右值=" << m << endl;
}
//第二种函数模板
void func(int &n){
cout << "左值=" << n << endl;
}
void func(int &&m){
cout << "右值=" << m << endl;
}
void func(double &n){
cout << "左值double=" << n << endl;
}
void func(double &&m){
cout << "右值double=" << m << endl;
}
template<class T>
void revoke(T &&t){
func(t);
}
第一种能正确输出左右值,但是,它并不能对不同类型的参数调用不同操作(不符合多态),假如你想要int类型的参数打印int,想要double类型的参数打印double,第一种函数无法实现。
第二种不能正确输出左右值,全部都走到左值,因为t是一个左值变量,只会被转发到左值函数中。
综上,只有用完美转发才能实现既识别左右值,又能对不同类型的参数调用不同操作。
通过forward<>()实现完美转发
void func(int &n){
cout << "左值int=" << n << endl;
}
void func(int &&m){
cout << "右值int=" << m << endl;
}
void func(double &n){
cout << "左值double=" << n << endl;
}
void func(double &&m){
cout << "右值double=" << m << endl;
}
template<class T>
void revoke(T &&t){
func(forward<T>(t));
}
int main(){
int i = 10;
int &n = i;
int &&m = 100;
m = 10;
//只能识别左右值,不能根据参数类型选择不同操作
// func(n);
// func(static_cast<int&&>(m));
//正确推导
revoke(static_cast<int&>(n));
revoke(static_cast<int&&>(m));
revoke(n);
revoke(move(m)); //移动语义,将m的内容移动给一个临时变量,此时传递的是将亡值,会被识别成右值。move之后m的资源被转移,不应该再次使用m
double d = 1.1;
double &dn = d;
double &&dm = 10.10;
revoke(static_cast<double&>(dn));
revoke(static_cast<double&&>(dm));
// //err -- 全部是左值
// revoke(n);
// revoke(m);
// //err -- 全部是右值
// revoke(static_cast<int>(n));
// revoke(static_cast<int>(m));
return 0;
}
输出如下:
走到这里,又引出三个问题。1.T&&是什么 2.forward原理 3.为什么要强转
1.T&&是什么:
有个东西叫引用折叠
引用折叠的规则:
如果两个引用中至少有一个是左值引用(&
),那么结果是左值引用(&
)
如果两个引用都是右值引用(&&
),那么结果是右值引用(&&
)
比如我们int & &&,那么它会引用折叠变成int &
int && &&,会变成int &&
引用折叠和模板很像,模板识别参数类型,引用折叠识别引用类型。比如我们template<class T>,那么输入int就会识别成int,输入double就会识别成double。在引用折叠下,输入什么值引用就会识别成什么值引用。
2.forward原理
forward用于保持原参数的类型,比如我是int&就保持int&,在第二种函数模板中,我们已经说过了,如果不保持原参数的类型,那么t就是一个左值变量,会全部走到左值
3.为什么要强转
查看完美转发的代码,可以发现有两个错误示例
//err -- 全部是左值
revoke(n);
revoke(m);
//err -- 全部是右值
revoke(static_cast<int>(n));
revoke(static_cast<int>(m));
第一个 -- 在函数调用时,如果你直接传递一个具名变量(无论它是通过左值引用还是右值引用声明的),该变量作为参数传递给函数时总是作为左值传递的。如果我们不强转,那么会被识别左值,这里的强转实际上是告诉函数,我想要传递的类型是什么
第二个 -- 当一个A类型变量强转为B类型时,实际上是创建了一个B类型的匿名对象来接收一个A类型的变量的值,而匿名对象是右值(将亡值),并且它不具名,所以会被识别成右值(只有具名变量才会默认作为左值传递)