c++中的右值引用的简单理解
以下的内容是我看了网上的博文后自己的总结,如果时间充裕的话,建议直接看原文。
简介:
c++中的右值引用十分不好理解,但是当你大概知道右值引用解决了c++中的什么问题后,会感觉右值引用还是很有用的。平时我们在c++中使用的引用为了和c++11引入的右值引用区分,一般把它称之为左值引用。左值引用很好理解,就是多个变量名绑定到了同一块内存上,操作这几个不同的变量名相当于操作同一块内存。但是右值引用就让人有点摸不着头脑了,一是右值(可以简单地认为就是字面常量或者临时变量)根本就没有变量名,这怎么引用?二是左值引用已经解决了按值传参的缺陷,右值引用又是干嘛的?正是由于这些问题让人感觉难以理解,我们需要分解问题,一步步去理解它。先复习一下什么是左值和右值。知道了左值右值的概念后,直接讲右值引用还是会感觉有点晦涩难懂,所以先引入一个move语义的概念,然后给出一个场景,再去看右值引用存在的意义。
左值和右值的区别:
区分左值和右值最简单的办法就是看一个内存空间是否可以使用&符号取得它的地址。能取到的就是左值,取不到的就是右值。比如
什么是move语义:
假设我们有一个这样的class:它里面包含着一个 int *m_memory 的成员变量,指向一块从堆上分配来的内存,并且假设这块内存有点大,复制一份这块内存中的字节需要一定的时间。
我们一般写一个重载了赋值运算符构造函数会这么写(记得深拷贝):
BaseClass :: operator=(const BaseClass &other)
{
释放该对象m_memory指向的内存
复制一份other中的m_memory的字节,并使该对象m_memory指向这块内存
}
然后拷贝构造函数也是一样的操作。
当我们在其他地方使用到BaseClass的时候,可能会出现这样的场景:
BaseClass func (); //假设定义了一个返回BaseClass对象的函数
BaseClass b;
b = func();
最后一行会发生如下操作:
<1> 调用b的赋值构造函数,释放b.m_memory和b中的其他资源, 把func()函数返回的临时对象中的m_memory和其他资源都复制一份到b中。
<2> 释放临时对象中的m_memory和其他资源
上面展示的就是常规的使用变量接受函数返回值的流程,并且这个流程没有问题,可以正常运行。但是可以发现,在上面的代码中,首先是b.m_memory被释放,b.m_memory重新malloc一块一样大的内存,然后把func()函数返回临时变量中的m_memory的字节拷贝一份给b.m_memory, 最后释放临时变量中的m_memory。如果我们换一种思路,把临时变量中的m_memory和b.m_memory指针交换一下,交换后的b.m_memory不用释放的同时省去了重新申请内存的操作。然后由临时对象释放交换后自身的m_memory。也就是把赋值构造函数和拷贝构造函数实现成这样:
BaseClass :: operator=(BaseClass &other)
{
swap(this.m_memory, other.m_memory);
}
这就是所谓的移动语义,move语义。
但是把赋值构造函数实现成这样有一个问题:
如果我们把赋值构造函数实现成这样,那下面的代码中的情况1怎么办?本来只是想赋值的,结果做了交换:
BaseClass b1;
BaseClass b2;
b2 = b1; // 情况1
BaseClass b3;
b3 = func(); //情况2
所以现在的问题就在于,我们想情况调用赋值构造函数。当我们传入的是左值时,我们希望调用拷贝内存这一版的赋值构造函数。当我们传入的是临时变量是,我们希望调用的是有move语义的赋值构造函数,就比如情况2中的代码。
右值引用:
这时就可以引入右值引用的概念了,我们可以重载两个赋值构造函数(拷贝构造函数一样)
BaseClass& operator=(const BaseClass& other); //内部实现为内存拷贝的拷贝构造函数
BaseClass& operator=(BaseClass&& other); //带有右值拷贝,内部实现为移动语义的拷贝构造函数。因为要交换,所以形参不能是const
右值引用的概念:如果BaseClass是一个类型,那么BaseClass&&就成为右值引用。c++在语法级别做了适应。就以拷贝构造函数为例,如果发现传入的参数是左值,则会调用常规的拷贝构造函数,如果发现传入的是临时变量,则会调用形参为右值引用的那个拷贝构造函数。如果是这样的话,上面的情况1和情况2中的代码就有救了,在c++11中,情况1会自动调用常规拷贝构造函数,而情况2因为传入的是临时变量,则会调用移动拷贝构造函数。
如果m_memory比较大,而且拷贝构造在代码中使用很频繁(比如在STL的容器以及各种对应算法中,拷贝构造相当频繁),移动拷贝构造函数可以极大地提高代码的性能。这时,c++11引入右值引用的意义也出来了,就是为了方便实现移动语义,提升c++的性能。
左值使用移动语义:
按照上面的逻辑,只有右值通过右值引用才可以使用移动语义。但是在一些情况下,左值也想使用移动语义怎么办,比如以下场景:
template<class T>
void swap(T& a, T&b)
{
T tmp(a);
a = b;
b = a;
}
swap中全部都是左值,所以无法像右值那样通过右值引用使用移动语义。但是如果这样的函数可以使用移动语义,岂止美滋滋?为了实现左值也能够使用移动语义,c++11引入了std::move函数。这个函数的作用就是把一个左值变成一个右值再返回来。所以swap函数可以改成这样:
template<class T>
void swap(T& a, T&b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
这时需要特别注意一个问题,以移动拷贝构造函数为例,对象使用move之后,那么这个对象就不应该再被使用了。因为对象传进去的时候本身就不是const,很有可能被具有移动语义的函数给改变了,比如情况1。后续的代码继续使用调用move之后的对象,一不小心就会写出很让人头大的bug。这也是为什么默认只有右值可以使用移动语义,因为右值一般使用了之后就会马上被释放掉。上面的swap代码中这几个变量在调用move后已经确认过不会再被后续的代码做复杂的使用,所以才可以放心地使用移动语义。
c++对于刚才说的这一点也有一些安全措施,比如下面的代码中,会直接调用BaseClass的常规拷贝构造函数:
void func(BaseClass&& x)
{
BaseClass y = x;
}
虽然形参是右值引用类型,但是c++认为,x在func的整个作用域内都是可见的,所以会把x当做左值来使用。在右值引用这一块,可以简单地认为,有名字的会被c++当成左值,没名字的会被c++当成右值。