右值引用

什么是右值

首先要明确什么是右值,对于下面的语句

1
int i = 42;

i为左值,42为右值。

右值引用

在c++中我们可以对变量进行引用操作,也就是左值引用,如下所示:

1
2
int i = 42;
int& j = i;

这样i和j实际上是同一变量的不同名字。那么右值引用也是类似的道理,右值引用是对右值的引用(有点废话),如下所示:

1
2
int i = 42;
int&& j = i * 2;

在这里i*2是一个右值,也是一个临时的计算结果,我们通过右值引用可以引用到这个右值。

这有什么用呢?我们完全可以用一个变量m = i * 42来得到这个值啊。在这个例子中实际上右值引用并没有任何意义,后面我们会看到它存在的实际意义。

右值引用的一些特殊示例

1
2
3
4
5
int i = 1;
int&& j = i++; // correct 因为i++是操作会首先返回i的值,然后再++,这是个右值,因此可以用右值引用
int&& k = ++i; // error 因为++i是先++,然后再返回的变量i,是个左值,因此这里是错误的
int& m = i++; // error 原理同上
int& n = ++i; // correct 原理同上

为什么要有右值引用

从上面的例子来看,完全看不到右值引用的价值。我们先看下右值的特点:
对于左值,左值是相对持久的(毕竟他是一个临时变量),但是右值却是短暂的。用完了就被销毁了。

右值引用只能绑定到临时对象,而这个临时对象很快就被销毁了,那么如果我们用临时变量来保存的话,会隐式执行拷贝。因此右值引用的存在可以减少不必要的拷贝,提高效率。

如下所示:

1
2
3
4
5
6
7
8
void test() {
vector<int> vs;
vs.resize(100000);
for (auto v : s) {
v = v + 1;
}
vector<int> vs1 = vs; // 这一行被导致拷贝的操作,如果vs比较大,会非常耗时
}

1
2
3
4
5
6
7
8
void test() {
vector<int> vs;
vs.resize(100000);
for (auto v : s) {
v = v + 1;
}
vector<int> vs2 = std::move(vs); // 右值引用,没有拷贝操作的发生,一样的达到了目的
}

std::move

右值引用,然而我们的右值大部分都是一些确定的内存空间或者一些很难有办法使用的临时结果,我们没有办法直接用,而能用的变量大多是左值,不能用于右值引用,因此标准库提供了std::move函数,让我们能通过变量拿到右值。一句话,std::move就是通过左值拿到右值。

1
2
int a = 0;
int&& b = std::move(a);

以及我们上面的例子,

1
2
3
4
5
6
7
8
void test() {
vector<int> vs;
vs.resize(100000);
for (auto v : s) {
v = v + 1;
}
vector<int> vs2 = std::move(vs);
}

说到了这里,右值引用感觉很有用,但是貌似也很鸡肋,这是因为我们的例子没有体现出它的价值,下面我们看他的引用场景

  • 移动构造函数
  • 移动赋值运算符

strVec的例子

移动构造函数和移动赋值函数在标准库的设计中是很有用的。c++ primer中有个例子就很好。我们知道c++提供了vector模板,可以自动扩展数组的大小,那么它是怎么做的呢?我们不需要提前指定数组的大小,vector会提前申请一块区域,当这块区域用完后,vector会申请另一块2被这个大小的区域,为了保持内存的连续性,它会将旧的数据从当前空间移动到新的空间(注意这里不是copy,而是移动)。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include "string"
#include "memory"

using namespace std;

class StrVec {
public:
StrVec() : elements(nullptr), firstFree(nullptr), capacity(nullptr) {}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
StrVec::StrVec(StrVec&& s) noexcept : elements(s.elements), firstFree(s.firstFree), capacity(s.capacity) {
s.elements = s.firstFree = s.capacity = nullptr;
}
StrVec& StrVec::operator=(StrVec&& rhs) noexcept {
if (this != &rhs) {
free();
elements = rhs.elements;
firstFree = rhs.firstFree;
capacity = rhs.capacity;
rhs.elements = rhs.firstFree = rhs.capacity = nullptr;
}
return *this;
}
~StrVec();

void push_back(const string&);

void push_back(const string&& s);

size_t size() const {
return firstFree - elements;
}

size_t capacity() const {
return capacity - elements;
}

string* begin() const {
return elements;
}

string* end() const {
return firstFree;
}

private:
static allocator<string> alloc;
void chn_n_alloc() {
if (size() == capacity()) {
reallocate();
}
}
void free();
void reallocate();
pair<string*, string*> alloc_n_copy(const string*, const string*);

private:
string* elements;
string* firstFree;
string* capacity;
}

void StrVec::push_back(const String& s)
{
ckn_n_alloc();
alloc.construct(firstFree++, s);
}

void StrVec::push_back(const string&& s)
{
chn_n_alloc();
alloc.construct(firstFree++, std::move(s));
}

pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e)
{
auto data = alloc.allocate(e - b);
return {data, uninitialized_copy(b, e, data)};
}

void StrVec::free()
{
if (elements) {
for (auto p = firstFree; p != elements; ) {
alloc.destroy(--p);
}
alloc.deallocate(elements, cvap - elements);
}
}

StrVec::StrVec(const StrVec& s)
{
auto newData = alloc_n_copy(s.begin(), s.end());
elements = newData.first;
firstFree = capacity = newData.second;
}

StrVec::~StrVec()
{
free();
}

StrVec& StrVec::operator=(const StrVec& rhs)
{
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
firstFree = capacity = data.second;
return *this;
}

void StrVec::reallocate()
{
auto newCapacity = size() ? 2* size() : 1;
auto newData = alloc.allocate(newCapacity);
auto dest = newData;
auto ele = elements;
for (size_t i = 0; i != size(); ++i) {
alloc.construct(dest++, std::move(*elem++));
}
free();
elements = newData;
firstFree = dest;
capacity = elements + newCapacity;
}
显示 Gitment 评论