1,左值右值定义
1,左值
左值是一个表示数据的表达式(如变量名或解引用的指针),一般具有持久状态 ,存储在内存中,左值可以出现在赋值符号左边/右边,可以获取一个左值的地址。
下述的"p","*p","b","c","s","s0"都是左值。
cpp
int* p=new int(1);
*p=10;
int b=1;
const int c=b;
string s("11111");
s[0]='x';
2,右值
右值也是一个表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象,右值只能出现在赋值符合右边,且不能取地址。
cpp
double x=1.2,y=1.3; //该x,y是左值
19;
x + y;
fmin(x,y);
string("1231"); //以上都是常见的右值
而能否取地址的规定是语言库中明文规定:"&"要求左值。那么为什么"右值"不能取地址?
从上述右值的示例来看,右值的用法就是短暂的,可见其生命周期必然是短暂的。
如字面量"19",对于编译器而言它不是一个对象,而是编译后直接编码在机器指令里当立即数或者放只读数据段。那么也不存在地址。
如表达式结果"x+y",cpu计算完之后会将结果放入寄存器,而寄存器没有内存地址,寄存器是cpu内部暂存的单元。
如临时对象"string("12333")",之所以叫临时对象,就是会在完整表达式结束那一刻销毁,那么此时取地址该指针会瞬间悬空。(我猜这也是不允许单独取地址的原因之一)

3,左值和右值的区别
由上述可见,左值和右值的主要区别在于**"能否取地址"**。另外说的是其英文翻译:
左值:lvalue---locate value,意为存储在内存中,有明确存储地址的对象。
右值:rvalue---read value ,则意为提供数据值,只读但不能寻址的对象。
2,左值引用和右值引用
无论左值/右值引用都是给对象起别名,不开空间。
左值引用就是给左值起别名,右值引用就是给右值起别名。
cpp
Type& n1=x; //左值引用
Type&& nn1=y; //右值引用
int&& nn2=10;
左值引用不能直接引用右值,但const左值引用可以引用右值;
右值引用不能直接引用左值,但右值引用可以直接引用move(左值);

move是库中一个函数模板,其内部本质是进行强制类型转换,且涉及一些引用折叠后续再谈。
cpp
//左值
int* p=new int(0);
int b=1;
int d=9;
const int c=b;
*p=10;
string s("111111");
s[0]='x';
int& lb=b; //左值引用给左值取别名
int*& lp=p;
int& lpp=*p;
string& ls=s;
char& lc=s[0];
//右值引用给右值取别名
int&& r1=10;
int&& r2=b+d; //需要注意此时的r1,r2,r3属性是左值
int&& r3=fmin(b,d);
string&& r4=string("111111");
//const左值引用可以给右值取别名
const int& lr1=10;
const string& lr2=string("1111");
const int& lr3=b+d;
//右值引用引用move
int&& rl1=move(b);
int*&& rl2=move(p); //也就是说 move(b) 是左值
int&& rl3=move(*p);
string&& rl4=move(s);
由上述提到 的变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式是左值。
cpp
int&& r1=10;
int&& rr1=move(r1);
3,引用延长生命周期
右值引用和const左值引用可以延长临时对象的生命周期,但const左值引用的这些对象不能被修改。
cpp
string s1="11111"; //s1是左值
const string& s2=s1+s1; //s1+s1是右值,这是const左值引用引用右值
string&& s3=s1+s1; //右值引用右值
s3+=s1; //"s1+s1"的生命周期延长
//s2+=s1; //错误,const左值不可修改,需维持const特性
4,左值和右值参数匹配
在C++98中,对于一个参数为const左值引用的函数,可以传左值和右值
在C++11中,若方便重载各个类型(左值引用,const左值引用,右值引用),则会根据传递的参数进行配对。
其中右值引用若没有被重载,则右值会调用const左值引用。
cpp
void fun(int& x)
{
cout<<"左值引用:"<<endl;
}
void fun(const int& x)
{
cout<<"const左值引用:"<<endl;
}
void fun(int&& x)
{
cout<<"右值引用:"<<endl;
}
int main()
{
int i=1; //左值
const int b=2; //const 左值
int&& x=2; //右值引用
fun(i); //fun(int&)
fun(b); //fun(const int&)
fun(1); //fun(int&&) 若没有则 fun(const int&)
fun(move(i));
fun(x); //fun(int&)
fun(move(x));
return 0;
}
5,左值引用使用场景
左值引用就是我们日常所说的引用,那么引用的主要使用场景在于函数的传参和返回值。引用的一个概念就是"对象别名",所以传参时传引用那么就相当于传别名,相比传值传参减少一次拷贝且能修改实参对象。传引用返回值时也是减少一次拷贝。(上述的传引用返回的对象生命周期都比函数的生命周期要长),左值引用已经解决了大部分的拷贝问题,但还有一些场景无法传引用返回(返回的局部对象在函数结束时销毁),只能传值再拷贝返回。(右值引用也没有解决这部分的问题)
6,移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,要求第一个参数是该类类型的右值引用。
移动赋值是一个赋值运算符的重载,与拷贝赋值构成函数重载,也要求第一个参数是该类类型的右值引用。
那么移动构造和移动赋值是什么形式?
cpp
class string
{
public:
void _swap(string& s)
{
swap(_str,s._str);
swap(_size,s._size);
swap(_capacity,s._capacity);
}
//移动构造
string(string&& s)
{
_swap(s);
}
//移动赋值
string& operator=(string&& s)
{
_swap(s);
return *this;
}
private:
char* _str=nullptr;
size_t _size=0;
size_t capacity=0;
};
如上述,移动构造和移动拷贝就是一种"资源掠夺",利用右值引用的特性,延长其生命周期,再掠夺其资源。(右值引用变量是左值),而只有对于类似string/vector这样包含深拷贝的类,移动构造和移动赋值才有意义,才能提高效率,因为这直接交换(swap)的效率高于拷贝资源的效率。
浅拷贝:只复制成员变量的值,比如成员变量是指针那么只会复制该指针地址而不管指针指向哪里,这样就会导致两个对象指向同一份资源导致后续的错乱。
深拷贝:对于指针变量,会重新申请一块空间,把指针指向的资源也拷贝一份,两个对象分别管理不同的一样的资源。
因此包含深拷贝的类,移动构造和移动赋值才有较大意义。
7,右值类型分类
C++11之后,对右值类型进一步进行了分类:右值分为纯右值(pure value---prvalue) 和将亡值(expiring value ---xvalue)。
纯右值:通常指的是字面常量,求值结果,无名临时对象。
将亡值:指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达。
如:move(x),(通常像生命周期延长的右值,且有名字)。
泛左值:泛左值包含将亡值和左值。

8,引用折叠
C++中不能直接定义引用的引用(意义参照指针的指针),但可以通过模板或typedef的类型操作就可以构成"引用的引用",也就是引用折叠。
cpp
int& && r1=i; //错误
使用引用折叠时C++11也给定了规则:
右值引用的右值引用折叠成右值引用,其他所有组合都折叠成左值引用。
不管是typedef还是类都用同一套规则,达到同一种"引用折叠"的形式。同时也需要符合前述左右值引用的规则。
cpp
//typedef ------引用折叠
typedef int& lref;
typedef int&& rref;
int n=0; //根据规定折叠之后,相当于:
lref& r1=n; //r1是int&
lref&& r2=n; //r2是int&
rref& r3=n; //r3是int&
rref&& r4=1; //r4是int&& --根据规则
//类 ---引用折叠
template<class T> //引用折叠规则限定:fun1实例化后总是一个左值引用
void fun1(T& x)
{}
template<class T> //fun2可以是左值引用也可以是右值引用
void fun2(T&& x)
{}
fun1<int>(0); //报错,fun1<int>相当于fun1(int& x),传右值(0)则会报错,以下类似。
fun1<int&>(0); //x
fun1<int&&>(0); //x
fun1<const int&>(0); //正确,(const int&)可以传右值
fun2<int>(0); //推导成(int&&)
fun2<int&>(n); //(int&)
fun2<int&&>(0); //(int&&)
9,完美转发
完美转发是一个函数模板forward,本质通过引用折叠的方式实现需要保持对象属性的一些场景。

在日常使用场景中,如下面的模板函数再调用另外一个函数,就可能会出现"不可控"的引用折叠,就会导致其无法达到想要效果,所以此时需要"完美转发forword",来保证对象t的属性。
cpp
template<class T>
T&& forward(remove_reference<T>& arg) noexcept
{
return static_cast<T&&>(arg);
}
void fun(int& x){ .. } / /..左值引用
void fun(const int& x){ .. } / /..const左值引用
void fun(int&& x){ .. } //..右值引用
void fun(const int&& x){ .. } / /..const右值引用
template<class T>
void function(T&& t)
{
fun(t);
//fun(forward<T>(t)); //作用位置
}
int main()
{
function(0); //传入右值,推导出T为int,最终类型为int&&
int a=1;
function(a); //传入左值,T为int&,引用折叠,最终为int&
function(move(a));
const int b=9;
function(b); //传入左值,T为const int&,引用折叠,最终为const int&
function(move(b));
return 0;
}