右值引用是个效率工具,减少拷贝。
C++11标准提出的一类数据类型。用于实现转移语义(move semantic)与完美转发(perfect forwarding)
1. 几个定义
int i = 5; // i为左值, 5为右值
int &j = i; // j为左值引用
int &&k = std::move(i+5); // k为右值引用
引用(reference)是指绑定到内存中的相应对象上。左值引用是绑定到左值对象上;右值引用是绑定到临时对象上。
设X为任何一种数据类型,则定义X&&是到数据类型X的右值引用(rvalue reference to X)。
传统的引用类型X&被称为左值引用(lvalue reference to X)。
右值引用与左值引用的变量都不能悬空(dangling),也即定义时必须初始化从而绑定到一个对象上。
2. 左值和右值
- 左值是对应(refer to)内存中有确定存储地址的对象的表达式的值,而右值是所有不是左值的表达式的值。
- 右值可以是字面量、临时对象等表达式。
- 左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。
3. 转移语义与std::move
C++在用临时对象或函数返回值给左值对象赋值时会产生拷贝,效率低。
如果把临时对象的内容直接移动(move)给被赋值的左值对象,效率改善将是显著的。这就是移动语义的来源。
所以转移语义与拷贝语义相对,类似电脑上文件的剪切与复制。
区分转移语义与拷贝语义用右值引用,有右值引用就是转移语义。
标准库函数std::move
可以把左值或临终值,转化为右值引用。
有一点值得注意的时具名的右值引用被当作左值。
例子如下:
#include <iostream>
#include <vector>
#include <utility>
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main()
{
int i = 5;
int &j = i;
int &&k = std::move(i+5);
process_value(j); // LValue
process_value(i + 5); // RValue
process_value(std::move(i)); // RValue. std::move把左值转化为右值引用
process_value(k); // LValue. 具名的右值引用被当作左值
return 0;
}
结果:
LValue processed: 5
RValue processed: 10
RValue processed: 5
LValue processed: 10
上面只是简单的例子,用到类里面,与传统的拷贝赋值运算符(copy assignment)成员函数、拷贝构造(copy ctor)成员函数对应,
移动语义需要有移动赋值(move assignment)成员函数、移动构造(move ctor)成员函数的实现机制。
可以通过函数重载来确定是调用拷贝语义还是移动语义的实现。
上面的例子似乎只是说明了转移语义和std::move
的用法,并没有看到转移语义带来的效率提升。
下面列举一个效率提升的例子:
#include <iostream>
#include <vector>
#include <utility>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
v.push_back(str); // copy. use push_back(const T&)
std::cout << "After copy, str is \"" << str << "\"\n";
v.push_back(std::move(str)); // move. use push_back(T&&)
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are : " << std::endl
<< v[0] << std::endl
<< v[1] << std::endl;
return 0;
}
结果:
After copy, str is "Hello"
After move, str is ""
The contents of the vector are :
Hello
Hello
第1种使用常量左值引用形式的push_back
,第2种使用右值引用形式的push_back
。
第1种用法,涉及到string
的拷贝,且对原str
不产生影响。
第2种用法,使用了转移语义,直接把原来的内容转移到vector
里去,被转移的str
对象处在一个“合法而未指定”的状态,所以为""。这样就不用涉及string
的拷贝,所以提高了效率。
4. 完美转发与std::forward
前面提到,具名的右值引用会被当作左值。
下面的fn()
的参数可以是右值引用,fn()
里调用了子函数overloaded()
, fn()
把右值引用参数转发给子函数overloaded()
,
如果没有完美转发,由于具名的右值引用会被当作左值,子函数overloaded()
接收到的参数始终是左值。
本可以接收右值引用参数的overloaded()
在这时没有提高传参效率。
std::forward
是用来做完美转发的函数,对原来的参数做原样转发。
#include <iostream>
#include <vector>
#include <utility>
#include <string>
// function with lvalue and rvalue reference overloads:
void overloaded(const int& x) { std::cout << "[lvalue]"; }
void overloaded(int&& x) { std::cout << "[rvalue]"; }
// function template taking rvalue reference to deduced type:
template <class T>
void fn(T&& x) {
overloaded(x); // always an lvalue
overloaded(std::forward<T>(x)); // rvalue if argument is rvalue
}
int main()
{
int a;
std::cout << "calling fn with lvalue: ";
fn(a);
std::cout << '\n';
std::cout << "calling fn with rvalue: ";
fn(0);
std::cout << '\n';
return 0;
}
结果:
calling fn with lvalue: [lvalue][lvalue]
calling fn with rvalue: [lvalue][rvalue]
overloaded()
可以接收左值引用参数,也可以接收右值引用参数。
第1次,fn()
的参数是左值引用,overloaded(x);
调用的是左值引用的函数,overloaded(std::forward<T>(x))
调用的也是左值引用的函数。
第2次,fn()
的参数是右值引用,overloaded(x);
调用的还是左值引用的函数,overloaded(std::forward<T>(x))
调用的则是与fn()
的参数一样的右值引用的函数。参数得到了完美转发。
为什么要把fn()
写成模板函数?
因为加了模板,fn()
就也可以使用左值去调用了,去掉template <class T>
,fn()
只接受右值引用的参数。
这里面涉及到引用折叠原则:
-
T& &
(引用的引用) 被转化成T&
-
T&& &
(rvalue的引用)被传化成T&
-
T& &&
(引用作rvalue) 被转化成T&
-
T&& &&
被转化成T&&
所以左值的参数传过来T&& &
被转化为T&
,右值参数传过来T&& &&
被转化成 T&&
,所以这样写之后,fn()
就既可以接收左值引用参数,也可以接收右值引用参数了。
5. 参考
https://zh.wikipedia.org/wiki/%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8
https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
https://zh.cppreference.com/w/cpp/utility/move
http://www.cplusplus.com/reference/utility/forward/