理解右值引用前,我们需要先理解什么是右值。右值(RValue)是指存在于内存上的,但是我们无法通过符号(或者叫别名)去访问修改的临时变量。e.g.
int k = 5; // k = LValue, 5 = RValue
int m = k + j; // m = LValue, k+j = RValue
右值引用,类似于左值引用,用来指向右值。下面例子就是通过赋值操作将右值42连接到引用j:
int &&j = 42;
有了右值引用,我们可以对42存储的内存位置进行读写。同时,通过右值引用,我们还延长了临时变量右值42的生命周期,只要j还在有效的scope里,右值42就是有效的。
引入右值引用的主要考虑之一就是节省拷贝操作,使得赋值操作更有效率。以下面的例子为例:
int m = k + l; // m = LValue, k+l = RValue
执行上面这条指令,会有以下几步:
因此,这行简单的计算赋值的花销是:内存上创建了两个变量,一次拷贝操作和一次释放操作。下面看一下使用右值引用的情况:
int &&n = k + l; // n = RValue-Reference
执行步骤:
可以看出,使用右值引用,只在内存上创建了一次对象,并且没有拷贝操作和释放操作。因此比没有使用右值引用要更高效。
跟左值引用一样,右值引用可以作为函数的参数,从而避免拷贝操作。看起来右值引用和左值引用似乎一样,都是通过提供一个别名指向内存上变量的位置来节省拷贝操作,唯一的区别就是右值引用指向的是内存上没有名字的临时变量,而左值引用指向的是内存上有名字的变量。但是在实际使用时,不能将右值赋给左值引用,也不能将左值赋给右值引用。看下面的例子:
int main()
{
int i{5};
int &lv_ref = i;
// int &lv_ref = 5; // Error: lvalue reference to type ‘int’ cannot bind to a temporary of type ‘int’
int &&rv_ref = 10;
// int &&rv_ref = i; // Error: rvalue reference to type ‘int’ cannot bind to lvalue of type ‘int’
rv_ref = 20;
cout << "rv_ref = " << rv_ref << endl; // rv_ref = 20
return 0;
}
从例子中可以看出,我们也可以使用右值引用来改变临时变量的值。
下面我们来看下编译器对左值和右值的判断:
void passValue(int &¶m)
{
cout << "Rvalue\n";
}
void passValue(int ¶m)
{
cout << "Lvalue\n";
}
int main()
{
int i{5};
int &&rv_ref = 10;
passValue(i); // output: Lvalue
passValue(5); // output: Rvalue
passValue(rv_ref); // output: Lvalue
return 0;
}
前面两个调用很好理解,i是左值,所以调用参数是左值引用的函数,5是右值,所以调用参数是右值引用的函数。比较有意思的是,入参是一个右值引用的话,调用的却是参数是左值引用的函数。编译器把右值引用看作是左值,这也不难理解,右值引用是指向内存上临时变量的别名,我们可以读取它也可以通过它来修改临时变量的值,因此右值引用在使用上和左值没有区别。所以,右值引用基本上是左值。
同样地,在void passValue(int &¶m)函数体内param也是一个左值:
// parameter is rvalue reference
void passValue(int &¶m)
{
// but here expression param has an lvalue value category
// can use std::move to convert it to an xvalue
}
如果给参数再加上const,那么情况会更复杂一点点:
// parameter is const lvalue reference
void fn(const X &) { std::cout<< "const X &\n"; }
int main()
{
X a;
fn(a); // works, argument is an lvalue
fn(X()); // also works, argument is an rvalue
}
也就是说const X&既可以接收左值,也可以接收右值。可以参考这两个解释 解释1 解释2
当然,如果X&, X&&, const X&三类参数都有各自的重载函数,那调用时会优先选择最匹配的那个,如果没有X&或X&&版本,那么会退而求其次调用const X&版本:
struct X {};
// overloads
void fn(X &) { std::cout<< "X &\n"; }
void fn(const X &) { std::cout<< "const X &\n"; }
void fn(X &&) { std::cout<< "X &&\n"; }
int main()
{
X a;
fn(a);
// lvalue selects fn(X &)
// fallbacks on fn(const X &)
const X b;
fn(b);
// const lvalue requires fn(const X &)
fn(X());
// rvalue selects fn(X &&)
// fallbacks on fn(const X &)
}
这条规则最典型的应用就是copy constructor/assignment, move constructor/assignment:
引用折叠,也就是Reference Collapsing。在理解引用折叠前,我们需要理解一个场景,就是当模板函数的参数中有模板参数的右值引用时,对函数参数类型的推导。
当模板函数的参数中有模板参数的右值引用时,这时函数参数使用起来并不是一个右值引用,它有一个新的名字,转发引用(forwarding reference)(具体可以参考C++标准委员会的文档:forwarding reference). 下面是一个判别是否是转发引用的例子:
template
void foo(T &&); // forwarding reference here
// T is a template parameter for foo
template
void bar(std::vector &&); // but not here
// std::vector is not a template parameter,
// only T is a template parameter for bar
当出现转发引用时,模板函数既可以接收左值,也可以接收右值。
当第一种情况出现时,函数参数就变成X& &&,这时就要引入引用折叠的规则:
借助下面的例子,我们再加深理解一下:
template
void fn(T &&) { std::cout<< "template\n"; }
int main()
{
X a;
fn(a);
// argument expression is lvalue of type X
// resolves to T being X &
// X & && collapses to X &
fn(X());
// argument expression is rvalue of type X
// resolves to T being X
// X && stays X &&
}
std::move主要是用来将变量转变成右值,从而可以使用move语义将变量"move"到别的地方。下面是一个使用std::move的例子:
struct X
{
std::string s_;
X(){}
X(const X & other) : s_{ other.s_ } {}
X(X && other) noexcept : s_{ std::move(other.s_) } {}
// other is an lvalue, and other.s_ is an lvalue too
// use std::move to force using the move constructor for s_
// don't use other.s_ after std::move (other than to destruct)
};
int main()
{
X a;
X b = std::move(a);
// a is an lvalue
// use std::move to convert to a rvalue,
// xvalue to be precise,
// so that the move constructor for X is used
// don't use a after std::move (other than to destruct)
}
所以,std::move并不做实际的move操作,它只是返回一个右值,从而使得真正干move活的重载函数能被编译器选中执行。
std::forward主要用在模板函数中,并且这个模板函数的参数是转发引用(forwarding reference)。在这个模板函数中,函数参数现在是一个左值,std::forward可以将它原本的值类型提取出来,然后传给下一个调用链,也就是所谓的完美转发(perfect forwarding)。
下面是一个使用std::forward的例子:
// without std::forward
template
void foo(T &&arg)
{
// arg is an lvalue object of the rvalue reference type.
// It calls the copy constructor.
// vector vect is copied element-by-element to the var variable.
std::vector var = arg;
....
}
std::vector vect(1'000'000, 1);
foo(std::move(vect));
// with std::forward
template
void foo(T &&arg)
{
// The std::forward function is called.
// It returns an xvalue object with the rvalue reference type.
// It calls the move constructor. vector vect is moved to the var variable.
std::vector var = std::forward(arg);
....
}
std::vector vect(1'000'000, 1);
foo(std::move(vect));
从例子中可以看出,使用std::forward可以提取出模板函数参数原本的类型,从而触发move操作,提高执行效率。
整理翻译自:
1.Learn About Rvalue References | Udacity
2.C++ std::move and std::forward
3.The std::forward function