章节目录

右值引用

本节阅读量:

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


左值引用概述

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

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

上面表格第一列表示左值引用想要引用的值。下面的例子对应第二行。

1
2
const int x = 1; // 不可修改的左值 
int& y = x; // 无法被左值引用用来进行初始化

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

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 移动构造函数和移动赋值函数

下一节