定义
是一种新的引用类型。可以来帮助解决unnecessary copying问题和实现完美转发。
当右手边是一个右值,那么左手边可以steal resources from right side,而不需要重新分配内存。
左值vs右值
- 左值:可以出现在operator=左边的量。
- 右值:只能出现在operator=右边的量。
以int为例:以string为例:1
2
3
4
5int a = 9;
int b = 4;
a = b;
b = a;
a + b = 42;//error a+b 是一个右值以complex为例1
2
3
4
5
6string s1("Hello");
string s2("world");
s1 + s2 = s2; //ok
cout << "s1:" << s1 << endl; //s1:Hello
cout << "s2:" << s2 << endl; //s2:world
string() = "World";从上面的例子来看,string和complex的临时对象可以出现在左边,这存在bug。1
2
3complex<int> c1(3,8),c2(1,0);
c1 + c2 = complex<int>(4,9);
complex<int>() = complex<int>(4,9);
但是临时对象一定是右值。
在看一个例子:1
2
3
4
5
6int foo(){
return 0;
}
int x = foo();
int *p = &foo(); //error 函数返回类型是右值
foo() = 7; //右值不能被赋值danms
右值引用
如果需要减小临时对象释放和新对象内存分配和拷贝的开销,可以使用右值引用。
右值引用拷贝构造的底层原理就是对指针的浅拷贝。
需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
1 | int num = 10; |
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
1 | int && a = 10; |
引用折叠 & 万能引用
- 引用折叠:C++中禁止reference to reference,所以编译器需要对四种情况(也就是L2L,L2R,R2L,R2R)进行处理,将他们“折叠”(也可说是“坍缩”)成一种单一的reference。
- 万能引用:T&&
- T && 碰到右值int &&, T匹配成int;
- T && 遇到左值 int ,也能匹配,T此时是int &。
- T && 碰到左值const int,T匹配为 const int &。
- T &&碰到左值const int *(指针类型), T匹配为const int *&
- T &&碰到左值const int * const(指针类型), T匹配为const int *const &
例如:
1 | template<typename T> |
万能引用把实参类型推导为:
1 | //case 1 |
完美转发
- 动机:看下面这个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32template<typename T>
void print(T & t){
std::cout << "Lvalue ref" << std::endl;
}
template<typename T>
void print(T && t){
std::cout << "Rvalue ref" << std::endl;
}
template<typename T>
void testForward(T && v){
print(v);//v此时已经是个左值了,永远调用左值版本的print
print(std::forward<T>(v)); //本文的重点
print(std::move(v)); //永远调用右值版本的print
std::cout << "======================" << std::endl;
}
int main()
{
int x = 1;
testForward(x); //实参为左值
testForward(std::move(x)); //实参为右值
}
// Lvalue ref
// Lvalue ref
// Rvalue ref
// ======================
// Lvalue ref
// Rvalue ref
// Rvalue ref
// ======================
用户希望testForward(x);最终调用的是左值版本的print,而testForward(std::move(x));最终调用的是右值版本的print。
可惜的是,在testForward中,虽然参数v是右值类型的,但此时v在内存中已经有了位置,所以v其实是个左值!(请仔细阅读这段话,保证你理解了)
所以,print(v)永远调用左值版本的print,与用户的本意不符。print(std::move(v));永远调用右值版本的print,与用户的本意也不符。只有print(std::forward
不难发现,本质问题在于,左值右值在函数调用时,都转化成了左值(也就是有名称,可以取地址),使得函数转调用时无法判断左值和右值。
在STL中,随处可见这种问题。比如C++11引入的emplace_back,它接受左值也接受右值作为参数,接着,它转调用了空间配置器的construct函数,而construct又转调用了placement new,placement new根据参数是左值还是右值,决定调用拷贝构造函数还是移动构造函数。
std::forward源码
1 | //接受左值 |
当我们传入T = int &,经过模板参数推导和引用折叠:
1
2
3constexpr int & && //折叠
forward(typename std::remove_reference<int &>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int & &&>(__t); } //折叠最终转化为:
1
2
3constexpr int & //折叠
forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &>(__t); } //折叠当我们传入std::move(int)
模板推导:1
2
3constexpr int &&
forward(typename std::remove_reference<int>::type& __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &&>(__t); }经过引用折叠:
1
2
3constexpr int &&
forward(int & __t) noexcept //remove_reference的作用与名字一致,不过多解释
{ return static_cast<int &&>(__t); }万能引用绑定到右值上时,不会发生引用折叠,所以这里没有引用折叠。