章节目录

右值引用

本节阅读量:

在前面,我们引入了值范畴的概念(左值和右值),这是表达式的一个属性,有助于确定表达式是否解析为值、函数或对象。我们还引入了左值和右值,以便我们可以讨论左值引用。


左值引用概述

在C++11之前,C++中只存在一种类型的引用,因此它被称为“引用”。然而,在C++11中,它被称为左值引用。左值引用只能用可修改的左值初始化。

左值引用 可以被初始化 可以被赋值
可修改的左值
不可修改的左值
右值

常量对象的左值引用可以用可修改和不可修改的左值和右值进行初始化。然而,不能修改这些值。

const左值引用 可以被初始化 可以被赋值
可修改的左值
不可修改的左值
右值

常量对象的左值引用特别有用,因为它们允许我们将任何类型的参数(左值或右值)传递到函数中,而无需复制参数。


右值引用

C++11添加了一种新的引用类型,称为右值引用。右值引用是设计为(仅)使用右值初始化的引用。使用单个与符号创建左值引用时,使用双与符号创建右值引用:

1
2
3
int x{ 5 };
int& lref{ x }; // 使用左值 x 初始化左值引用
int&& rref{ 5 }; // 使用右值 5 初始化右值引用

右值引用不能用左值初始化。

右值引用 可以被初始化 可以被赋值
可修改的左值
不可修改的左值
右值
const右值引用 可以被初始化 可以被赋值
可修改的左值
不可修改的左值
右值

右值引用有两个有用的属性。首先,右值引用将其被初始化对象的寿命延长到右值引用的寿命(对常量对象的左值引用也可以这样做)。其次,非常量右值引用允许您修改右值!

让我们看一些例子:

 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
#include <iostream>
 
class Fraction
{
private:
	int m_numerator { 0 };
	int m_denominator { 1 };
 
public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
	}
 
	friend std::ostream& operator<<(std::ostream& out, const Fraction& f1)
	{
		out << f1.m_numerator << '/' << f1.m_denominator;
		return out;
	}
};
 
int main()
{
	auto&& rref{ Fraction{ 3, 5 } }; // 对 临时Fraction对象的右值引用
	
	// f1 的 operator<< 绑定在右值引用上,无需创建临时对象
	std::cout << rref << '\n';
 
	return 0;
} // rref (和临时的 Fraction 对象) 在这里超出作用域

该程序打印:

1
3/5

作为匿名对象,Fraction(3,5)通常会在定义它的表达式末尾超出作用域。然而,由于我们用它初始化右值引用,因此它的持续时间被延长到代码块的末尾。然后,我们可以使用该右值引用来打印Fraction的值。

现在,让我们看一个不太直观的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>

int main()
{
    int&& rref{ 5 }; // 使用字面值来创建右值引用
    rref = 10;
    std::cout << rref << '\n';

    return 0;
}

该程序打印:

1
10

虽然用字面值初始化右值引用,然后能够更改该值似乎很奇怪,但当用字面值初始化右值引用时,临时对象是从字面值构造的,因此引用的是临时对象,而不是字面值。

当然,右值引用通常不以上述的任何一种方式来使用。


右值引用作为函数参数

右值引用通常用作函数参数。当您希望左值和右值参数具有不同的行为时,这对于函数重载最有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

void fun(const int& lref) // 左值会调用这个函数
{
	std::cout << "l-value reference to const: " << lref << '\n';
}

void fun(int&& rref) // 右值会调用这个函数
{
	std::cout << "r-value reference: " << rref << '\n';
}

int main()
{
	int x{ 5 };
	fun(x); // 调用左值版本
	fun(5); // 调用右值版本

	return 0;
}

这将打印:

1
2
l-value reference to const: 5
r-value reference: 5

可以看到,当传递左值时,重载函数解析为具有左值引用的版本。当传递右值时,重载函数解析为具有右值引用的版本。

为什么要这么做?我们将在下一课中更详细地讨论这一点。不用说,它是移动语义的重要组成部分。


右值引用变量是左值

请考虑以下代码:

1
2
	int&& ref{ 5 };
	fun(ref);

您希望上面的函数调用哪个版本的fun: fun(const int&)或 fun(int&&) ?

答案可能会让你吃惊。它调用 fun(const int&)。

尽管变量ref的类型为int&&,但在表达式中使用时,它是一个左值(与所有命名变量一样)。对象的类型及其值类别是独立的。

您已经知道,字面值 5 是int类型的右值,而 int x; 是int类型的左值。类似地,int&& ref;是 int&& 类型的左值。

因此,fun(ref)不仅调用fun(const int&),它甚至不匹配 fun(int&&) ,因为右值引用不能绑定到左值。


返回右值引用

您几乎不应该返回右值引用,因为同样的原因,您几乎不应返回左值引用。在大多数情况下,当被引用对象在函数末尾超出作用域时,会得到悬空引用。


22.0 智能指针和移动语义简介

上一节

22.2 移动构造函数和移动赋值函数

下一节