左值和右值
在C++11之前,我们很少去关注左值和右值这一概念,但是在C++11中,加入了一个非常重要的语法:右值引用。
左值和右值,一般来说可以当作字面意思,左值是经常出现在表达式左边的值,右值是经常出现在表达式右边的值。但是,仅仅靠这一判断方式是绝对错误的。左值和右值的本质区别是能否被取地址,能被取地址的值为左值,不能被取地址的值为右值。
我们也可以换一种方式来定义左值和右值:
暂时不会被销毁的值为左值
即将被销毁的值为右值
如何理解这一定义?
我们在匿名对象中学到,我们传值或者赋值的时候可以用匿名对象来传值,这个匿名对象的生命周期只在这一行,这一行过后匿名对象便会被销毁
而在学习完类与对象之后,我们可以把所有内置类型的赋值都抽象为一个匿名对象的赋值
而这些生命周期极短,没有一个具体的变量名称(即为匿名),无法被其他地方调用的值便称为右值。
同样,在函数中,也有一些甚至不出现在文本中的右值
此时我们便可以很清晰的发现一个结论:
右值引用
右值引用的定义
我们知道,C++中为了避免函数传参的时候反复赋值,我们要尽可能去使用引用传参来提高效率。但是对于返回值的右值情况,我们并没有很好的办法去进行引用传参。因为如果返回值是一个引用返回,我们必须new一个新空间来返回,在大多数情况是不满足需求的。而如果采用一般的传值返回,因为右值无法取地址,无法进行引用,就不可避免会进行拷贝右值,降低效率。
于是在C++11中,就有了一个新语法:右值引用。顾名思义,右值引用是一种可以引用右值的语法,当我们进行右值引用的时候,这个右值的空间便可以被我们访问,同样该右值的空间也可以被我们修改,就相当于给将死之人一段新的寿命,让其生命周期延长一段时间直到定义右值引用时的名称也被销毁为止
右值引用的长相如下:
我们可以随意访问右值的空间
但是右值引用的使用场景并非在此,我们无需过多关注右值引用的那块空间是如何变化的
右值引用的使用场景——移动构造
我们在进行拷贝构造的时候,会涉及一个问题——浅拷贝和深拷贝。浅拷贝好说,把所有值全部赋值过来便可以了,但是深拷贝,比如所有的container,我们要重新开辟节点,对每一个节点赋值,效率一下就下来了
而且一般的拷贝构造是无奈之举,如果我们用一串右值进行拷贝构造,右值本来就是一个临时变量,我们拷贝完这个右值,接着又销毁这个右值,那岂不是多此一举?
这个时候便开始怀疑和思考:
这个时候,右值引用便迎来了他的高光时刻
我们再来思考一下我们的需求:
- 如果传入值是左值,我们深拷贝这个左值,左值不发生任何变化
如果传入值是右值,我们直接将这个右值塞到对象里,不需要管右值的死活 - 也就是说,我们只需要做两件事:识别传入的是左值还是右值,如何以最高效的方法将右值塞到对象里。
对于第一个问题, 很简单,我们只需要用重载函数将参数写为右值引用,这样这一函数接口只能识别右值
对于第二个问题,也很简单,因为右值的死活我们不关心,我们干脆直接交换右值和对象便可以了
这样,只需要一次交换,右值的值便全部到了左值身上,而因为右值马上会销毁,所以其为任何值都没有关系,我们也不需要关心,如此便把一个深拷贝的问题变成了一个单个值交换的问题
而因为swap是需要左右值都是可以修改的,所我们传入参数不能传入const的右值
效率上的意义
光只了解移动构造,其实并没有多大的价值,因为我们平时也不太常用初始化列表进行构造。但是,对于函数的传参,这个意义就大了
还是刚刚的例子
如果我们定义了右值引用的构造函数,在作为右值的临时变量去构造新的string时,便会调用移动构造而非深拷贝,通过这种方式来提高传值的效率
移动语义
此刻,我们已经接触到了一个概念:移动构造。但是移动构造仍然会出现一些小问题,例如:
class A
{
public:
A()
{}
//深拷贝构造
A(const A& a)
{
cout << "深拷贝构造" << endl;
}
//移动构造
A(A&& a)
{
cout << "移动构造" << endl;
}
};
//右值引用
void func(A&& right_val)
{
//这个是移动构造还是深拷贝构造?
A tmp(right_val);
}
int main()
{
func(A());
}
我们在一个函数中传入右值引用,但是我们用该右值来构造的时候,我们惊奇地发现,最终调用的是深拷贝构造
原因其实很简单。我们在左右值的定义中已经说到,左右值的区别是其能否被取地址,或者其是否是即将被销毁的变量,但是这里的right_val,虽然是一个右值引用,但是它已经成为了一个类型为A&&的新变量,而且其只有在函数走完后才会被销毁,其完完全全是一个左值
所以自然,我们用其进行构造的时候,当然会调用传入左值时的拷贝构造函数,而非移动构造函数。
有没有一些方法来解决呢?当然
Move
右值引用,听名字就知道,只可以引用右值,无法引用左值。
但是我们非要引用左值,怎么办?
这个时候便要介绍一个新的函数:move
move的作用是传入左值后返回一个右值,但是其只是返回值是个右值,并非将左值的属性改成了右值,不过返回值的右值和原本的左值享有共同一块空间,只有属性发生了变化
int main()
{
int a = 1;//没毛病
int&& b = 2;//没毛病
int&& c = move(a);//现在没毛病了
int&& d = a;//a仍是一个左值,仍无法被右值引用
//验证右值的a和左值的a共用一块空间
swap(b, c);
cout << a;
cout << b;
cout << c;
}
对于move的具体实现,我们就不要去细究了,我们只需要直到以上的概念,总结为:
- move的作用是传入左值,只更改过属性的右值
- 传入的值和返回的值共享同一块区间
- 传入后左值仍为左值,不会变为右值
所以,我们想采用右值引用时,只需要加一个move便可以实现
//右值引用
void func(A&& right_val)
{
//右值引用
A tmp(move(right_val));
}
int main()
{
func(A());
}
完美转发
除了move,C++还提供了另一个函数——forward来进行类型的识别。
看了看字典的解释,便可以简单理解forward的意思
forward函数用于保留其在传参过程中原生类型的属性,比如int&&的原生类型属性是int的右值,而非int&&的左值
//右值引用
void func(A&& right_val)
{
//右值引用
A tmp(forward(right_val));
}
int main()
{
func(A());
}
如此一来,如果传入的是左值,那便会采用左值的深拷贝构造,如果传入的是右值,便会采用右值的移动构造,就避免了move不管左值右值最终都转化为右值的问题
那就有人要问了,这不只有传入右值吗?传入左值怎么通过这个接口呢?
那你就问到点了,这便是C++的另一新语法:万能引用
万能引用
在模板里,我们使用typename&&作为参数时,并不是代表其是一个右值引用传参,而是代表着其左值引用传参和右值引用传参都可以接收
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
以上便是右值引用和移动语义的所有内容因为不涉及太多代码,就不放代码链接了