c++右值引用和std::move

c++右值引用和std::move

什么是左值、右值

左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边

什么是左值引用、右值引用

左值引用

左值引用就是指向左值的引用,左值引用不能指向右值

1
2
3
int a = 0;
int& b = a; //b是一个左值引用
int& c = 6; //编译错误,因为6是一个右值,左值引用不能绑定在右值上

左值引用不能指向右值,因为左值引用允许修改被引用变量的值,但是右值没有地址,无法修改

const左值引用可以指向右值,因为const左值引用无法修改指向对象的值,所以STL许多函数的参数都是const type&

右值引用

右值引用就是可以指向右值的引用,右值引用不能指向左值

1
2
3
4
int&& a = 6;//a是一个右值引用
int b = 5;
int&& c = b;//错误,b是一个左值
a = 7;//右值引用允许修改右值

std::move函数

std::move函数是将一个左值转换成右值,函数本身和移动没有关系,仅仅是把左值转换成了右值

1
2
3
int a = 6;
int& b = a;
int&& c = std::move(a);//通过std::move把一个左值转换成了右值

因为std::move的作用仅仅是转换,所以性能上不会有任何提升

右值引用本身是左值还是右值

先说结论:被声明出来的,有名称的右值引用其本身就是左值,因为其有地址,其他的是右值。也可以说作为函数返回值的 && 是右值,直接声明出来的 && 是左值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void fun(int&& n)
{
n = 1;
}

int main()
{
int a = 5;
int& b = a;
int&& c = std::move(a);

fun(a);//错误,a是一个左值
fun(b);//错误,b是一个左值引用
fun(c);//错误,c本身是一个左值

fun(std::move(a));//正确
fun(std::move(b));//正确
fun(std::move(c));//正确
fun(6);//正确
}

std::move函数的应用场景

std::move函数是将一个左值转换成右值,仅仅有转换功能,函数本身和移动没有关系,因此std::move函数本身并不能提高性能,需要配合移动构造函数移动赋值函数来实现移动语义从而避免产生深拷贝来提升性能。

因此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
65
66
67
68
69
class Obj
{
public:
//默认构造函数
Obj()
{
mem = nullptr;
len = 0;
}
//构造函数
Obj(size_t len)
{
this->len = len;
mem = (char*)(malloc(len));
memset(mem, 1, len);
}
//析构函数
~Obj()
{
if(mem)
free(mem);
}
//拷贝构造函数
Obj(const Obj& other)
{
len = other.len;
mem = nullptr;
if(len > 0)
{
mem = (char*)(malloc(len));
memcpy(mem, other.mem, other.len);
}
}
//拷贝赋值函数
Obj& operator =(const Obj& other)
{
if(mem)
free(mem);
len = other.len;
mem = nullptr;
if(len > 0)
{
mem = (char*)(malloc(len));
memcpy(mem, other.mem, other.len);
}
return *this;
}
//移动构造函数
Obj(Obj&& other)
{
len = other.len;
mem = other.mem;
other.len = 0;
other.mem = nullptr;
}
//移动赋值函数
Obj& operator =(Obj&& other)
{
len = other.len;
mem = other.mem;
other.len = 0;
other.mem = nullptr;
return *this;
}
private:
char* mem;
size_t len;
};

类中维护了一段内存,如果调用拷贝构造和拷贝赋值就会重新申请内存并写入数据,如果调用移动构造和移动赋值就仅会把mem的所有权给移动一下。相比之下移动构造和移动赋值函数的性能是比拷贝构造和拷贝复制的性能要高的。但是有一点要记住就是被移动的对象再移动完之后内部的资源都会被清空

例如有以下调用函数

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
int main()
{
int allCount = 1000000;
size_t perSize = 10;
std::vector<Obj> v;
v.reserve(allCount);

v.resize(0);
auto start = std::clock();
for(size_t i = 0; i < allCount; i++)
{
Obj obj(perSize);
v.push_back(obj);
}
printf("push_back time:%lfs\n", ((double)(std::clock()-start))/CLOCKS_PER_SEC);

v.resize(0);
start = std::clock();
for(size_t i = 0; i < allCount; i++)
{
Obj obj(perSize);
v.push_back(std::move(obj));
}
printf("move push_back time:%lfs\n", ((double)(std::clock()-start))/CLOCKS_PER_SEC);

return 0;
}

第一个循环用了普通的push_back会调用拷贝构造函数,第二个循环先用std::move把左值转换成右值然后再调用push_back函数就会调用移动构造函数,但是每个循环内部的obj在push_back后其中的数据也被清空了。经过实现第二个循环的效率比第一个循环要高。

上述代码在Compiler Explorer的运行:

还有一些对象是只允许移动不允许拷贝的,比如unique_ptr,这种对象只允许移动内部资源所有权

完美转发std::forward

std::forward与std::move一样也是类型转换,但是std::move只能转出来右值,而std::forward可以转出来左值或右值

std::forward(u)有两个参数:T与 u。 1. 当T为左值引用类型时,u将被转换为T类型的左值; 2. 否则u将被转换为T类型右值。

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
void fun1(int&& n)
{
n = 2;
}

void fun2(int& n)
{
n = 2;
}

int main()
{
int n = 0;
int&& nr = std::move(n);
fun1(nr);//错误 nr是一个左值
fun1(std::move(nr));//正确 转换成了右值
fun1(std::forward<int>(nr));//正确 转换成了右值

fun2(nr);//正确 nr是一个左值
fun2(std::move(nr));//错误 左值引用无法绑定到右值上
fun2(std::forward<int&>(nr));//正确 转换成了左值引用

return 0;
}

评论