右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。
从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…
在标准C++语言中,临时量(术语为右值,因其出现在赋值表达式的右边)可以被传给函数,但只能被接受为const &类型。这样函数便无法区分传给const &的是真实的右值还是常规变量。而且,由于类型为const &,函数也无法改变所传对象的值。C++0x将增加一种名为右值引用的新的引用类型,记作typename &&。这种类型可以被接受为非const值,从而允许改变其值。这种改变将允许某些对象创建转移语义。比如,一个std::vector,就其内部实现而言,是一个C式数组的封装。如果需要创建vector临时量或者从函数中返回vector,那就只能通过创建一个新的vector并拷贝所有存于右值中的数据来存储数据。之后这个临时的vector则会被销毁,同时删除其包含的数据。有了右值引用,一个参数为指向某个vector的右值引用的std::vector的转移构造器就能够简单地将该右值中C式数组的指针复制到新的vector,然后将该右值清空。这里没有数组拷贝,并且销毁被清空的右值也不会销毁保存数据的内存。返回vector的函数现在只需要返回一个std::vector<>&&。如果vector没有转移构造器,那么结果会像以前一样:用std::vector<> &参数调用它的拷贝构造器。如果vector确实具有转移构造器,那么转移构造器就会被调用,从而避免大量的内存分配。
一. 定义
通常意义上,在C++中,可取地址,有名字的即为左值。不可取地址,没有名字的为右值。右值主要包括字面量,函数返回的临时变量值,表达式临时值等。右值引用即为对右值进行引用的类型,在C++98中的引用称为左值引用。
如有以下类和函数:
1
2
3
4
5
6
7
8
9
10
|
class
A
{
private
:
int
* _p;
};
A ReturnValue()
{
return
A();
}
|
1
2
3
|
A& a = ReturnValue();
// error: non-const lvalue reference to type 'A' cannot bind to a temporary of type 'A'
const
A& a2 = ReturnValue();
// ok
|
1
|
A&& a3 = ReturnValue();
|
二. 移动语义
右值引用可以引用并修改右值,但是通常情况下,修改一个临时值是没有意义的。然而在对临时值进行拷贝时,我们可以通过右值引用来将临时值内部的资源移为己用,从而避免了资源的拷贝:
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
32
33
34
35
36
|
#include<iostream>
class
A
{
public
:
A(
int
a)
:_p(
new
int
(a))
{
}
// 移动构造函数 移动语义
A(A&& rhs)
: _p(rhs._p)
{
// 将临时值资源置空 避免多次释放 现在资源的归属权已经转移
rhs._p = nullptr;
std::cout<<
"Move Constructor"
<<std::endl;
}
// 拷贝构造函数 复制语义
A(
const
A& rhs)
: _p(
new
int
(*rhs._p))
{
std::cout<<
"Copy Constructor"
<<std::endl;
}
private
:
int
* _p;
};
A ReturnValue() {
return
A(5); }
int
main()
{
A a = ReturnValue();
return
0;
}
|
运行该代码,发现Move Constructor被调用(在g++中会对返回值进行优化,不会有任何输出。可以通过-fno-elide-constructors关闭这个选项)。在用右值构造对象时,编译器会调用A(A&& rhs)形式的移动构造函数,在移动构造函数中,你可以实现自己的移动语义,这里将临时对象中_p指向内存直接移为己用,避免了资源拷贝。当资源非常大或构造非常耗时时,效率提升将非常明显。如果A没有定义移动构造函数,那么像在C++98中那样,将调用拷贝构造函数,执行拷贝语义。移动不成,还可以拷贝。
std::move:
C++11提供一个函数std::move()来将一个左值强制转化为右值:
1
2
|
A a1(5);
A a2 = std::move(a1);
|
std::move乍一看没什么用。它主要用在两个地方:
- 帮助更好地实现移动语义
- 实现完美转发(下面会提到)
考虑如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class
B
{
public
:
B(B&& rhs)
: _pb(rhs._pb)
{
// how can i move rhs._a to this->_a ?
rhs._pb = nullptr;
}
private
:
A _a;
int
* pb;
}
|
这一点在后面的完美转发还会提到。现在我们可以用std::move来将rhs._a转换为右值:_a(std::move(rhs._a)),这样将调用A的移动构造。实现移动语义。当然这里我们确信rhs._a之后不会在使用,因为rhs即将被释放。
三. 完美转发
如果仅仅为了实现移动语义,右值引用是没有必要被提出来的,因为我们在调用函数时,可以通过传引用的方式来避免临时值的生成,尽管代码不是那么直观,但效率比使用右值引用只高不低。
右值引用的另一个作用是完美转发,完美转发出现在泛型编程中,将模板函数参数传递给该函数调用的下一个模板函数。如:
1
2
3
4
5
|
template
<
typename
T>
void
Forward(T t)
{
Do(t);
}
|
考虑到避免拷贝,我们可以传递引用,形如Forward(T& t),但是这种形式的Forward并不能接收右值作为参数,如Forward(5)。因为非常量左值不能绑定到右值。考虑常量左值引用:Forward(const T& t),这种形式的Forward能够接收任何类型(常量左值引用是万能引用),但是由于加上了常量修饰符,因此无法正确转发非常量左值引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void
Do(
int
& i)
{
// do something...
}
template
<
typename
T>
void
Forward(
const
T& t)
{
Do(t);
}
int
main()
{
int
a = 8;
Forward(a);
// error. 'void Do(int&)' : cannot convert argument 1 from 'const int' to 'int&'
return
0;
}
|
基于这种情况, 我们可以对Forward的参数进行const重载,即可正确传递左值引用。但是当Do函数参数为右值引用时,Forward(5)仍然不能正确传递,因为Forward中的参数都是左值引用。
下面介绍在 C++11 中的解决方案。
PS:引用折叠
C++11引入了引用折叠规则,结合右值引用来解决完美转发问题:
1
2
3
|
typedef
const
int
T;
typedef
T& TR;
TR& v = 1;
// 在C++11中 v的实际类型为 const int&
|
1
2
3
4
|
T& + & = T&
T& + && = T&
T&& + & = T&
T&& + && = T&&
|
再谈转发
那么上面的引用折叠规则,对完美转发有什么用呢?我们注意到,对于T&&类型,它和左值引用折叠为左值引用,和右值引用折叠为右值引用。基于这种特性,我们可以用 T&& 作为我们的转发函数模板参数:
1
2
3
4
5
|
template
<
typename
T>
void
Forward(T&& t)
{
Do(
static_cast
<T&&>(t));
}
|
当传入左值引用 X& 时:
1
2
3
4
|
void
Forward(X& && t)
{
Do(
static_cast
<X& &&>(t));
}
|
1
2
3
4
|
void
Forward(X& t)
{
Do(
static_cast
<X&>(t));
}
|
1
2
3
4
|
void
Forward(X&& && t)
{
Do(
static_cast
<X&& &&>(t));
}
|
1
2
3
4
|
void
Forward(X&& t)
{
Do(
static_cast
<X&&>(t));
}
|
在C++11中,static_cast<T&&>(t) 可以通过 std::forward<T>(t) 来替代,std::forward是C++11用于实现完美转发的一个函数,它和std::move一样,都通过static_cast来实现。我们的Forward函数最终变成了:
1
2
3
4
5
|
template
<
typename
T>
void
Forward(T&& t)
{
Do(std::forward<T>(t));
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include<iostream>
using
namespace
std;
void
Do(
int
& i) { cout <<
"左值引用"
<< endl; }
void
Do(
int
&& i) { cout <<
"右值引用"
<< endl; }
void
Do(
const
int
& i) { cout <<
"常量左值引用"
<< endl; }
void
Do(
const
int
&& i) { cout <<
"常量右值引用"
<< endl; }
template
<
typename
T>
void
PerfectForward(T&& t){ Do(forward<T>(t)); }
int
main()
{
int
a;
const
int
b;
PerfectForward(a);
// 左值引用
PerfectForward(move(a));
// 右值引用
PerfectForward(b);
// 常量左值引用
PerfectForward(move(b));
// 常量右值引用
return
0;
}
|
四. 附注
左值和左值引用,右值和右值引用都是同一个东西,引用不是一个新的类型,仅仅是一个别名。这一点对于理解模板推导很重要。对于以下两个函数
1
2
3
4
5
6
7
8
9
10
11
|
template
<
typename
T>
void
Fun(T t)
{
// do something...
}
template
<
typename
T>
void
Fun(T& t)
{
// do otherthing...
}
|
Fun(T t)和Fun(T& t)他们都能接受左值(引用),它们的区别在于对参数作不同的语义,前者执行拷贝语义,后者只是取个新的别名。因此调用Fun(a)编译器会报错,因为它不知道你要对a执行何种语义。另外,对于Fun(T t)来说,由于它执行拷贝语义,因此它还能接受右值。因此调用Fun(5)不会报错,因为左值引用无法引用到右值,因此只有Fun(T t)能执行拷贝。
最后,附上VS中 std::move 和 std::forward 的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// move
template
<
class
_Ty>
inline
typename
remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
return
((
typename
remove_reference<_Ty>::type&&)_Arg);
}
// forward
template
<
class
_Ty>
inline
_Ty&& forward(
typename
remove_reference<_Ty>::type& _Arg)
{
// forward an lvalue
return
(
static_cast
<_Ty&&>(_Arg));
}
template
<
class
_Ty>
inline
_Ty&& forward(
typename
remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{
// forward anything
static_assert(!is_lvalue_reference<_Ty>::value,
"bad forward call"
);
return
(
static_cast
<_Ty&&>(_Arg));
}
|