右值引用
1. 基本含义
C++中所有的表达式都是左值或者右值。右值编译器管它叫rvalue,左值编译器叫它lvalue。具体定义没太研究,但是他们有以下区别:
- 左值与右值的区别是左值具名,可以取址 并访问
- 右值不具名,通常是临时的变量,常数等。不可取址,仅在当前作用域有效,可以被移动。
一些常见的左值和右值的例子如下图。
2. 用处
首先,左值,左值引用晓得吧。左值引用的一个example如下。通过左值引用,我们可以:
- 在函数内外共用同一个变量,可以实现函数内外的值同步改变.
- 也可以减少实例的拷贝带来的时间和内存的消耗。
class A{
int a;
public:
A(int num){
a = num;
}
A& operator=(const A& a){}
void set(A &a){
this->a = a.a;
}
}
int main(){
A a(3);
A b(2);
a.set(b);
return 0;
}
在上述example中,b就是一个左值,是一个具名变量,而如果你直接调用func(A())
就会报错,因为A()
是一个临时变量(右值),在传参结束后就被销毁了,因此也不能引用了。此时就需要引入右值引用,右值引用在形参中用&&
表示
class A{
int a;
public:
A(int num){
a = num;
}
A& operator=(const A& a){}
void set(A &a){
this->a = a.a;
}
void set(A &&a){
this->a = a.a;
}
void func(int && num){
a = num;
}
}
int main(){
A a(2);
a.set(A(3));
a.func(3); // 3 是常数,也是右值
return 0;
}
当然,上述的两个set函数在功能上是有重复的,我们完全可以把他们合并,这里就要提到std::move()
函数了。
3. std::move()让左值变成右值
std::move()是让一个左值变成右值,比如下面的代码也是完全合法的。
class A{
int a;
public:
A(int num){
a = num;
}
A& operator=(const A& a){}
void set(A &&a){
this->a = a.a;
}
void func(int && num){
a = num;
}
}
int main(){
int c = 4;
A a(2);
A b(3);
a.set(A(3));
a.set(std::move(b)); //没有std::move会报错
a.func(3); // 3 是常数,也是右值
a.func(std::move(c)); //没有std::move会报错
return 0;
}
读完这段代码再想想,仅仅依靠左值引用,上面的功能是无法实现的。
4. 用右值引用实现类型推断
当一个函数的形参是模板类型的右值引用时,那么实参的类型会进行自动推断(而不要求必须是右值),比如下面代码中的调用都是合法的。注意在正常情况下如果一个形参是右值引用,那么给函数传递一个左值是会有编译错误的,但是使用模板类型的右值引用就不会有这个问题。
class A{
int a;
public:
A(int num){
a = num;
}
A& operator=(const A& a){}
template<typename T>
void set(T &&a){
this->a = a.a;
}
}
int main(){
A a(3);
A b(4);
a.set(b); // 合法,b是一个左值,传递进函数后依然是左值
a.set(A(4)); // 合法传递一个右值
}
5. 完美转发
一个表达式是左值还是右值,这个性质是会发生变化的,在上述例子中,实参A(3)
虽然是右值,但是一旦传递给函数后,在set()函数内部,它就变成了左值,因为它现在不在是一个临时变量,而是成了set函数内部的局部变量。
完美转发指的是表达式被传递时能够原封不动地被转发,这里所说的原封不动,指的是变量的值、是否 const、是否为左/右值的属性均不能发生改变。完美转发需要依靠三样东西来同时完成:模板类型、std::forward()、右值引用。下面是一个example:
class A{
int a;
public:
A(int num){
a = num;
}
A& operator=(const A& a){}
template<typename T>
void set(T &&a){
this->a = a.a;
set2(std::forward(a));
}
template<typename T>
void set2(T &&a){}
}
在上述函数中,set函数的调用传参可以是左值、也可以是右值,可以含有const,可以不含const,无论是何种情况调用都是合法的,并且set函数调用set2时,会完美转发a,如果传递进set函数的是右值,那么传递给set2的依然是右值,其他属性也一样。
6. 左值和右值模板的类型推断规则
& | && | |
---|---|---|
T& | & | & |
T&& | & | && |
上述表格中T&代表形参是左值引用的模板类型,T&&代表形参是右值引用的模板类型,表头&和&&分别表示实参是左值还是右值,对应表中实际的类型,可以看到
- 当形参为左值时,无论传入的参数是什么,它实际都会变成左值。
- 当形参为右值时,实际的参数由实参决定
注意
右值引用过程中涉及到一个可能的隐患,即内存有效性问题。这里提供了一个例子:
在上述拷贝构造中,为了安全,我们必须将实参str的data设置为null,方式构造函数调用结束后str被析构,它申请的内存变为无效的情况发生。例子详见https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
参考文献
- 移动语义与完美转发 | Universal Reference https://www.sczyh30.com/posts/C-C/cpp-move-semantic/
- 右值引用于转移语义:https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
- 移动语义与完美转发 https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/