右值引用

R-Value

在第一章我们提到了左值和右值,然后告诉你不要太担心它们。在C++ 11出现之前这么说没什么问题。但是要理解C++ 11中的Move语义则需要重新审视该主题。这就是本节的内容。

左值和右值

尽管左值和右值名称中都包含“值”一词,但左值和右值实际上不是值的属性,而是表达式的属性。

C++中的每个表达式都有两个属性:一个类型(用于类型检查)和一个值类别(Value Category)(用于某些类型的语法检查,例如是否可以将表达式的结果用于赋值)。在C++ 03及更早版本中,左值和右值是唯一可用的两个值类别。

定义哪些表达式是左值哪些是右值是非常复杂的,因此我们将用比较简单的形式来讲解,这对我们的学习目的是足够了的。

定义左值(也称为定位器值)最简单的方法是将左值视为函数或对象(或一个计算为函数或对象的表达式)。所有左值都分配了内存地址。

左值的最初定义是“在赋值表达式的左侧的值都叫做左值”。然而,在const关键字被添加到了C++语言之后,这一定义就不准确了,并且左值被分成两个子类:可修改的左值和不可修改的左值(const)。

定义右值最简单的方法是“只要不是左值,那它就是右值”。这包括字面值(例如5),临时的值(例如x+1)和匿名对象(例如 Fraction(5, 2))。右值通常根据它们的值进行分辨,它具有表达式范围,且不能被赋值。 This non-assignment rule makes sense, because assigning a value applies a side-effect to the object. Since r-values have expression scope, if we were to assign a value to an r-value, then the r-value would either go out of scope before we had a chance to use the assigned value in the next expression (which makes the assignment useless) or we’d have to use a variable with a side effect applied more than once in an expression (which by now you should know causes undefined behavior!).

为了支持Move语义,C++ 11引入了3个新的值类别:pr-values, x-values, and gl-values。我们将在很大程度上忽略这些,因为理解它们对于有效地学习或使用移动语义是没有必要的。如果您有兴趣,cppreference.com有一个广泛的表达式列表,这些表达式符合各种值类别的要求,以及有关它们的更多详细信息。

左值引用

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

左值引用 可初始化 可修改
可修改的左值 Yes Yes
不可修改的左值 No No
右值 No No

可以使用左值和右值来初始化对const对象的左值引用。但是,这些值无法修改。

左值const引用 可初始化 可修改
可修改的左值 Yes No
不可修改的左值 Yes No
右值 Yes No

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

右值引用

C++ 11添加了一种称为右值引用的新类型引用。右值引用是一个只能用右值初始化的引用。使用单个&符号创建左值引用,但使用双&符号创建右值引用:

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

无法使用左值初始化右值引用。

右值引用 可初始化 可修改
可修改的左值 No No
不可修改的左值 No No
右值 Yes Yes
右值Const引用 可初始化 可修改
可修改的左值 No No
不可修改的左值 No No
右值 Yes Yes

右值引用有两个有用的属性。首先,右值引用将它们初始化的对象的生命周期延长到右值引用的生命周期(对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
#include <iostream>

class Fraction
{
private:
int m_numerator;
int m_denominator;

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()
{
Fraction &&rref = Fraction(3, 5); // r-value reference to temporary Fraction
std::cout << rref << '\n';

return 0;
} // rref (and the temporary Fraction) goes out of scope here

该程序输出:

3/5

作为一个匿名对象,Fraction(3,5)通常会在定义它的表达式的末尾超出范围。但是,由于我们用它初始化一个右值引用,它的持续时间会延长到块的结尾。然后我们可以使用该右值引用来输出Fraction的值。

现在让我们来看一个不太直观的例子:

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

int main()
{
int &&rref = 5; // because we're initializing an r-value reference with a literal, a temporary with value 5 is created here
rref = 10;
std::cout << rref;

return 0;
}

该程序输出:

10

虽然使用文字值初始化右值引用然后能够更改该值可能看起来很奇怪,但是当使用文字初始化右值时,从文字构造临时值以使引用引用临时值对象,而不是文字值。

右值引用不经常以上述任何一种方式使用。

右值引用作为函数参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void fun(const int &lref) // l-value arguments will select this function
{
std::cout << "l-value reference to const\n";
}

void fun(int &&rref) // r-value arguments will select this function
{
std::cout << "r-value reference\n";
}

int main()
{
int x = 5;
fun(x); // l-value argument calls l-value version of function
fun(5); // r-value argument calls r-value version of function

return 0;
}

这输出:

左值引用const
右值引用

如您所见,当传递左值时,重载函数已解析为具有左值引用的版本。当传递右值时,重载函数解析为具有右值引用的版本(这被认为是比对const的左值引用更好的匹配)。

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

一个有趣的说明:

1
2
int &&ref = 5;
fun(ref);

实际上调用函数的左值版本!虽然变量ref具有对整数的类型右值引用,但它本身实际上是左值(与所有命名变量一样)。混淆源于在两种不同背景下使用术语右值。可以这样想:命名对象是左值。匿名对象是右值。命名对象或匿名对象的类型与左值还是右值无关。或者,换句话说,如果已经调用了右值引用,则不会存在这种混淆。

返回右值引用

你永远都不应该在函数中返回一个右值引用,因为当引用的对象超出函数末尾的范围时你得到的就是一个无效的引用。