右值引用

右值

C++11 增加了一个新的类型,称为右值引用(R-value reference),标记为 &&
介绍右值引用类型之前,先了解什么是左值和右值:
(1)左值 lvalue(loactor value)
是指存储在内存中、有明确存储地址的数据(可以取地址)。
(2)右值 rvalue(read value)
是指可以提供数据值的数据(不可以取地址)。

区分左值与右值的便捷方法:
(1)对表达式取地址 & 就是左值,否则为右值。
(2)所有带名字的变量或对象都是左值,而右值是匿名的。

1
2
3
4
5
int a = 520;
int b = 1314;
a = b;
// a、b 为左值,520、1314 为右值
// `a = b` 是一种特殊情况,这个表达式中 a、b 都是左值,因为变量 b 是可以被取地址的,不能视为右值。

C++11 中右值可以分为两种:
(1)纯右值(prvalue, PureRvalue)
非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等。
(2)将亡值(xvalue, expiring value)
与右值引用相关的表达式,比如:T&& 类型函数的返回值、std::move 的返回值等。

1
int value = 520;

在上面的语句中,value 是左值,520 是字面量也就是右值。其中 value 可以被引用,520 不能被引用,因为字面量都是右值。

右值引用

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活。

关于右值引用的使用,示例代码如下:

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
#include <iostream>
using namespace std;

int&& value = 520; // 520 是纯右值,value 是对字面量 520 这个右值的引用

class Test
{
public:
Test()
{
cout << "construct" << endl;
}
Test(const Test& a)
{
cout << "copy construct" << endl;
}
};

Test getObj()
{
return Test();
}

int main()
{
int a1;
// int &&a2 = a1; // error:a1 虽然写在了 = 右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
// Test& t = getObj(); // error:右值不能给普通的左值引用赋值。
Test&& t2 = getObj(); // ok:返回的临时对象被称为将亡值
const Test& t3 = getObj(); // ok:常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。

return 0;
}

性能优化

在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价就非常大。在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

示例代码如下:

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
#include <iostream>
using namespace std;

class Test
{
public:
Test() : m_num(new int(100))
{
cout << "construct" << endl;
}

Test(const Test& a) : m_num(new int(*a.m_num))
{
cout << "copy construct" << endl;
}

// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num)
{
a.m_num = nullptr;
cout << "move construct" << endl;
}

~Test()
{
delete m_num;
cout << "destruct" << endl;
}

int* m_num;
};

Test getObj()
{
Test t;
return t;
}

int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;

return 0;
};
// g++ .\13.右值引用.cpp -o app -fno-elide-constructors

调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间。
如果要执行这样的操作需要使用右值引用,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象,这样能减少不必要的临时对象的创建、拷贝以及销毁,可以提高 C++ 应用程序的性能。

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝。如果不使用移动构造,在执行 Test t = getObj(); 的时候进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存被析构,对象 t 无法访问这块内存地址。

在测试程序中 getObj() 的返回值就是一个将亡值(右值)。在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,将临时对象中的堆内存地址的所有权转移给对象 t,这块内存被成功续命,因此在 t 对象中还可以继续使用这块内存。
对于需要动态申请大量资源的类,应该设计移动构造函数,提高程序效率。注意:一般在提供移动构造函数的同时,也提供常量左值引用的拷贝构造函数,保证移动不成还可以使用拷贝构造函数。

&& 的特性

在 C++ 中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中。如果是模板参数需要指定为 T&&,如果是自动类型推导需要指定为 auto&&,在这两种场景下 && 被称作未定的引用类型。
注意:const T&& 表示一个右值引用,不是未定引用类型。

在函数模板中使用 &&,示例代码如下:

1
2
3
4
5
6
7
8
template<typename T>
void f(T&& param);
void f1(const T&& param);

f(10); // 传入的实参 10 是右值,因此 T&& 表示右值引用
int x = 10;
f(x); // 传入的实参是 x 是左值,因此 T&& 表示左值引用
f1(x); // 参数 const T&& 不是未定引用类型,不需要推导,本身就表示一个右值引用

参考资料

https://subingwen.cn/cpp/rvalue-reference/


右值引用
https://lcf163.github.io/2021/09/14/右值引用/
作者
乘风的小站
发布于
2021年9月14日
许可协议