本文主要分析右值引用中的:移动语意(move semantics)。
要想理解右值,首先得能够判断具体什么是右值,先来看一些关于右值的判定条件:
一、任何表达式不是左值就是右值,左值和右值只是针对表达式定义的。
这个比较容易理解,int temp = 10, func(), double a = 0.0, x++, ++x, *ptr,x+y这些都是表达式,他们不是左值就是右值。
二、右值的生存期只到表达式结束,即语句的分号之后右值的生存期就结束了。
三、能够对左值取地址,但无法对右值取址。
四、左值能够在赋值表达式的左边和右边,但是右值无法放在赋值表达式的左边。
看完上述定义应该可以对右值有点了解了吧,它是一个只能放在赋值表达式右边的临时值。
为什么要提出右值这么个复杂的概念,原因是很多代码中生成了很多临时变量,在生成临时变量的时候无法避免地增加了分配内存和释放内存的开销(对于内存较大或内存分配频繁时开销很大),这种时候没必要再为左值重新分配内存,只需要把右值中大块内存的指针地址赋值给左值的指针即可。
这种情况类似于浅拷贝(shallow copy),不同之处在于浅拷贝没有把等号右边值的指针变为nullptr,右值(临时变量)在析构的时候将内存释放掉,左值指针指向的内容被释放掉了。但本质上来说,右值的移动语意是对浅拷贝语意的完善,减少内存的分配次数。
我们来分析几个具体例子。
第一个是关于自加符号的。
int t = 10; // 左值
++t; // 左值
t++; //右值
第一行定义了t之后,t明显是个左值,能够对t进行取址,能够对其赋值,也能将其赋值给其他的变量。其生存周期直到定义它的函数结束,而不是在“;”之后就结束了。
首先++t是一个表达式,这个表达式是一个左值,其表达式过程是先将t加1之后,然后将t返回,表达式返回的实际上还是t,因此它是左值。因此我们能够使用(++t) = 2;这种操作。
t++是一个右值,我们知道t++返回了t的值之后然后再加1。表达式在最后返回时是t的值,实际过程是先复制一个t_copy,然后将t的值加1,最后将t_copy返回,这样才能保证返回的是最开始t的值。t++表达式返回的是copy的临时变量,因此它是一个右值。因此(t++)=2;这种操作是没有的。
这也是为什么很多人喜欢写++t的原因,因为它少了一次复制的开销,虽然这种开销可能并不明显。
第二个是关于字符串相加的。
string final_str = str1 + ", " + str2 + ", " + str3;
string operator+(const string &s1, const string &s2); //函数1
string operator+(string && s1, string & s2); //函数2
我们知道字符串能够相加是对operator+进行了重载,重载的函数需要返回一个临时变量,假设如上面代码所示,只定义函数1时。计算上述final_str时,就在operator+函数中生成了4个临时string对象。对于(str1 + ", ")这个表达式(记作temp1),实际上就是一个临时变量(即右值)。后续操作是这个临时变量(temp1)加上str2之后再生成一个临时变量(temp2),以此类推生成temp3、temp4。
但是实际上我们能够简化这个步骤,当生成了temp1之后,把str2的值直接加在temp1后面即可,不需要生成temp2,整个过程只需要生成一个temp1即可。对于(str1 + ", ")这个右值,我们只需要重载包含右值引用的operator+就能够实现上述功能。
理解了右值的作用之后,需要看看c++11中增加的std::move()函数。
为什么需要这个函数?当函数为右值的时候不是可以自动重载吗?
std::move()主要是为了解决一个问题:明确的表明将左值作为右值。
void func(string && str); // 函数1
void func(string & str); // 函数2
string a;
func(a+a);
func(a);
对于func(a+a);明显直接调用第一个函数,因为a+a是一个临时的右值。但是很多时候有这种情况,即a在调用完func(a)之后就不再使用了,等到函数结束后a会被释放。从func(a)到a被释放这段时间里,a可能占用了大块内存却没使用。
这种时候不需要再在函数func(a)里面深拷贝a了,直接把a里面分配的内存给str就行了。这个时候我们需要强制调用第一个函数,但是a又是个左值。怎么办?std::move(a)这时就起作用了,它将a转换为右值,然后调用第一个函数,减少了一次大内存的分配。func(std::move(a));就解决了我们的问题。