[c++]
1.左值和右值
1.1左值和右值定义
在c++中,左值是一个指向内存的东西,换句话来讲,左值有地址
,保存在内存中,右值则为不指向任何地方东西,即不在内存中占有确定位置
。一般来说,右值是暂时和短暂的,而左值则存活的很久。如下例子
int var = 4;4 = var; //error
(var + 10) = 4; //error
其中,赋值运算符要求一个lvalue作为它的左操作数,当然var是一个左值,因为它是一个占确定内存空间的对象。
常量4
和表达式var+1都
不是lvalue(它们是rvalue)。它们不是lvalue,因为都是表达式的临时结果,没有确定的内存空间(换句话说,它们只是计算的周期驻留在临时的寄存器中)。
1.2右值细分
右值又可以分为纯右值,和将亡值。
在c++98中,右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值
。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。
在c++11中对c++98进行了扩充,在c++11中右值又分为纯右值和将亡值,其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值
;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
将亡值
可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
2.左值引用和右值引用
2.1左值引用
使用语法:类型 &表达式
左值引用
就是对一个左值进行引用的类型。引用必须初始化,可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名。
例子
char c_val = 'c';
char *ptr = &c_val;
char &r_val = c_val;
不要混淆 取地址 和 引用,当&说明符前面带有类型声明,则是引用,否则就是取地址。
通俗来说 &在 ”=” 号左边的是引用,右边的是取地址。
2.2右值引用
使用语法 类型 &&表达式
int num = 10;
//int && a = num; //右值引用不能初始化为左值,这里的num是一个左值
int && a = 10;
a = 100;
cout << a << endl;
// 输出结果为100,右值引用可以对右值修改
右值引用通常用于移动语义和完美转发。
其中,c++移动语义可以参考这篇文章,C++11的移动语义。
3.万能引用和完美转发
3.1万能引用
首先来看一个例子
#include <iostream>using std::cout;
using std::endl;template<typename T>
void func(T& param) {cout << param << endl;
}int main() {int num = 2019;func(num);return 0;
}
这里的编译输出都没有问题,但是我们修改为如下方式
int main() {func(2019);return 0;
}
编译时会出错,因为func传入了一个右值,而他只接受左值或者是左值引用。这里我们重载一个可以接受右值的函数模板,可以实现我们想要的效果
template<typename T>
void func(T& param) {cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {cout << "传入的是右值" << endl;
}int main() {int num = 2019;func(num);func(2019);return 0;
}
输出为
传入的是左值
传入的是右值
一次函数调用的是左值得版本,第二次函数调用的是右值版本。但是,有没有办法只写一个模板函数即可以接收左值又可以接收右值呢?
C++ 11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值。
但是注意了:只有发生类型推导
的时候,T&&才表示万能引用;否则,表示右值引用。类型推导阅读链接
所以,上面的案例我们可以修改为:
template<typename T>
void func(T&& param) {cout << param << endl;
}int main() {int num = 2019;func(num);func(2019);return 0;
}
3.2引用折叠
万能引用说完了,接着来聊引用折叠(Reference Collapse),因为完美转发(Perfect Forwarding)的概念涉及引用折叠。一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:
- 左值-左值 T& & # 函数定义的形参类型是左值引用,传入的实参是左值引用
- 左值-右值 T& && # 函数定义的形参类型是左值引用,传入的实参是右值引用
- 右值-左值 T&& & # 函数定义的形参类型是右值引用,传入的实参是左值引用
- 右值-右值 T&& && # 函数定义的形参类型是右值引用,传入的实参是右值引用
但是C++中不允许对引用再进行引用
,对于上述情况的处理有如下的规则:
所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
即就是前面三种情况代表的都是左值引用,而第四种代表的右值引用。
// ...
int a = 0;
int &ra = a;
int & &rra = ra; // 编译器报错:不允许使用引用的引用!
// ...
既然不允许使用,为啥还要有引用折叠这样的概念存在 ?!
原因就是:引用折叠的应用场景不在这里!!
下面我们介绍引用折叠在模板中的应用:完美转发
。
参考阅读,引用折叠与完美转发
3.3完美转发
那么,什么情况下,我们需要一个函数,既能接收左值,又能接收右值呢?
答案就是:转发的时候。
首先来看一个例子
#include <iostream>using std::cout;
using std::endl;template<typename T>
void func(T& param) {cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {cout << "传入的是右值" << endl;
}template<typename T>
void warp(T&& param) {func(param);
}int main() {int num = 2019;warp(num);warp(2019);return 0;
}
猜一下上述的输出
传入的是左值
传入的是左值
是不是和我们预期的不一样,下面我们来分析一下原因:
warp()
函数本身的形参是一个万能引用,即可以接受左值又可以接受右值
;第一个warp()
函数调用实参是左值
,所以,warp()
函数中调用func()
中传入的参数也应该是左值
;第二个warp()
函数调用实参是右值
,根据上面所说的引用折叠规则
,warp()
函数接收的参数类型是右值引用
,那么为什么却调用了调用func()的左值版本
了呢?这是因为在warp()函数内部,左值引用类型变为了右值,因为参数有了名称,我们也通过变量名取得变量地址。这里为什么在内部变了,可以参考3.3中的链接,里面有详细的解释。
所以说,会存在一些特殊的情况使得我们的右值在某些函数的内部变为了左值,而我们为什么需要完美转发,就是想要让传入的右值在整个流程中都保持右值的状态不改变。那么如何实现呢?这就是完美转发技术。在c++11中通过std::forward()函数实现,于是我们修改warp函数
template<typename T>
void warp(T&& param) {func(std::forward<T>(param));
}
既可以得到我们想要的结果。