rvalue_and_std::move

参考文章https://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

问题引出

一个例子

problem.cc

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
#include <iostream>
#include <string.h>
class String {
public:
String(const char *s) {
std::cout << "initlize\n";
size_ = strlen(s) + 1;
data_ = new char[size_];
strcpy(data_, s);
}

~String() {
std::cout << "call deconstructor\n";
delete []data_;
}

// copy
String(const String &s) {
std::cout << "call copy constructor\n";
size_ = s.size_;
data_ = new char[size_];
memcpy(data_, s.data_, size_);
}

friend std::ostream& operator<<(std::ostream& out, const String &s) {
if (s.size_ == 0 || s.data_ == NULL) {
return out;
}
out << s.data_;
return out;
}
public:
int size_;
char *data_;
};

String test() {
String str("hello");
return str;
}
int main(void) {
String s = test();
std::cout << "in main function\n";
return 0;
}

让我们编译运行一下:

需要注意使用编译选项 -fno-elide-constructors关闭RVO(Return Value Optimization),否则现在的编译器会给你自动优化

1
g++ problem.cc -o a.out -fno-elide-constructors

运行的结果如下:

1
2
3
4
5
6
7
第一行 initlize
第二行 call copy constructor
第三行 call deconstructor
第四行 call copy constructor
第五行 call deconstructor
第六行 in main function
第七行 call deconstructo

我们可以看到该程序调用了两次拷贝构造函数和三次析构函数。那么这个程序的流程是怎么样的呢?下面作者将对每一行输出做出解释。

输出解释

第一行

在调用test函数之后,String str("hello")会调用构造函数构造对象str ,对应输出的结果initlize

第二行

test函数返回str的时候,它返回的是一个临时对象,这个临时对象是通过对str拷贝而构造的,而不是直接返回的str本身。因为str只是test函数中的局部变量,它的作用域只在test函数间,不可能跳到函数外再传给主函数中的s,因此test函数返回的只是一个临时变量。

第三行

上面已经阐明,test函数返回的只是一个临时变量。所以当test函数退出的时候,局部变量str将会被析构。对应第三行的输出。

第四行

main函数中,test返回的临时变量会通过拷贝构造函数赋值给s,因此对应第四行中的拷贝构造函数。

第五行

当临时变量复制给s之后,这个临时变量也就被析构了。因此对第五行的输出。

第六行

本行的输出在临时变量析构之后发生,在s析构之前输出。

第七行

main函数中的局部变量s析构。

问题说明

我们可以看到,test函数本来应该返回的str,但其实返回的是一个临时变量,这就导致了不必要的拷贝构造。

那么我们有什么办法来解决这个问题呢?将test函数的返回值变成引用类型String &行不行呢?

这显然是不行的,因为str只是一个临时变量,当test函数结束的时候就会被释放,如果引用一个将被释放的值可能会出错。

因此对于临时变量这一问题,C++引入了右值(rvalue)和std::move这两个概念。

右值(rvalue)和std::move

什么是右值呢,它与左值相对于。左值也就是我们的变量之类的,它可以放在等号的左边。而右值可以理解为临时变量,C++使用&&作为右值引用的符号。而std::move可以将左值变为右值。

算了,不想写了,就这样吧。把代码贴上。

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
#include <iostream>
#include <string.h>
#include <string>
class String {
public:
String(const char *s) {
std::cout << "initlize\n";
size_ = strlen(s) + 1;
data_ = new char[size_];
strcpy(data_, s);
}

~String() {
std::cout << "deallocate string:" << "\n";
delete []data_;
}

// copy
String(const String &s) {
size_ = s.size_;
data_ = new char[size_];
memcpy(data_, s.data_, size_);
std::cout << "allocate and copy string\n";
}

// move
String(String &&s) {
size_ = s.size_;
data_ = s.data_;
s.size_ = 0;
s.data_ = nullptr;
}

friend std::ostream& operator<<(std::ostream& out, const String &s) {
if (s.size_ == 0 || s.data_ == NULL) {
return out;
}
out << s.data_;
return out;
}
public:
int size_;
char *data_;
};

String test() {
String str("test");
return str;
}

int main(void) {
String s("hello");
// 调用move constructor
String b = std::move(s);
// s的内容被移走了
std::cout << b << ", " << s << "\n";

// std::string也同样实现了move constructor
std::string ss = "hle";
std::string bb(std::move(ss));
std::cout << bb << std::endl;
String a("hello");
return 0;
}