Skip to content

15.2 右值引用

By Alex on February 20th, 2017 | last modified by Alex on January 23rd, 2020

翻译by dashjay 7-17

回到第一章我们讨论了左值和右值,然后告诉你不要太担心他们。在 C++11 之前,这是一个公平的建议。但是理解 C++11 中的移动语义需要对这个主题进行重新学习。所以现在就开始吧。

左值和右值

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

C++中的每个表达式都有两个属性:一个类型(用于类型检查)和一个值类别(用于某些类型的语法检查,例如表达式的结果是否可以分配)。在C++ 03和更早的时候,左值和右值是仅有的两个可用的值类别。

对于哪些表达式是左值,哪些是右值的实际定义非常复杂,因此我们将对这个主题采取简化的观点,这在很大程度上满足了我们的目的。

简单点可以认为左值(也称为定位器值[locator value])是函数或对象(或计算为函数或对象的表达式)所有左值都分配了内存地址。

当左值最初被定义时,它们被定义为“适合位于赋值表达式左侧的值”。然而,后来,const关键字被添加到语言中,并且左值被分为两个子类:可修改的左值(可以更改)和不可修改的左值(const)。

最简单的方法是把右值看作“不是l值的所有东西”。这特别包括字面值(例如 5 )、临时值(例如 x+1 )和匿名对象(例如 Fraction(5, 2))。右值通常是针对其值进行求值的,具有表达式范围(它们在其所在表达式的结尾处消亡),并且不能被赋值。这个非赋值规则是有意义的,因为赋值会给对象带来副作用。因为右值有表达式范围,如果我们要给右值赋值,那么右值会在我们有机会在下一个表达式中使用上次赋值之前就超出了范围(这使得赋值毫无用处),要么我们必须使用一个变量,在一个表达式中多次应用一个副作用(现在您应该知道这会导致未定义的行为!)。

为了支持移动语义,C++ 引入了三个新的值类型:pr-values,x-values 和 gl-values. 我们大概率会忽略这些知识,因为高效的学习和使用移动语义并不需要理解他们。如果你感兴趣,cppreference.com 有一个广泛的表达给每一个变量类型,同时有更多关于他们的细节 L-value references

在 C++ 之前,C++ 中只有一个类型的引用,并且因此它仅仅被叫做引用。然而,在 C++11 中,它有时候也被叫做 l-value 引用。L-value 引用只能由可修改的左值来初始化。

L-value reference Can be initialized with Can modify
Modifiable l-values Yes Yes
Non-modifiable l-values No No
R-values No No

指向常对象的 l-value 可以被使用 l-value 和 r-value 来初始化,然而,这些值不能被修改。

L-value reference to const Can be initialized with Can modify
Modifiable l-values Yes No
Non-modifiable l-values Yes No
R-values Yes No

常对象的左值引用非常有用,因为他们允许你传任何类型的参数(l-value 或 r-value)进入一个函数,而无需拷贝一份。

R-value 引用

C++ 添加了一个新的叫做右值引用(r-value reference)。右值引用是被设计来只能使用右值初始化的。左值是引用使用一个 & 创建的,而右值引用使用两个 & 来创建。

int x{ 5 };
int &lref{ x }; // l-value reference initialized with l-value x
int &&rref{ 5 }; // r-value reference initialized with r-value 5
R-value reference Can be initialized with Can modify
Modifiable l-values No No
Non-modifiable l-values No No
R-values Yes Yes
R-value reference to const Can be initialized with Can modify
Modifiable l-values No No
Non-modifiable l-values No No
R-values Yes No

右值引用有两个很有用的性质。第一,右值引用扩展了原初始化值的生命周期到右值引用的生命周期(指向常对象的左值引用也可以做到)。第二,非常量引用允许你修改右值。

让我们再来看一个例子:

# 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()
{
 auto &&rref{ Fraction{ 3, 5 } }; // r-value reference to temporary Fraction

    // f1 of operator<< binds to the temporary, no copies are created.
    std::cout << rref << '\n';

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

程序打印

3/5

作为一个匿名对象,Fraction(w3, 5) 将会正常的在定义它语句结束时离开作用域。然而,因为我们以它来初始化了一个右值引用,他的声明周期就持续到了语句块结束。我们紧接着可以使用右值引用来打印 Fraction 的值。

好的,我们来看一个更加直观的例子:

# 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 << '\n';

    return 0;
}

程序打印

10

使用一个字面值来初始化一个右值,然后能够改变那个右值。这是因为,当使用字面值来初始化右值,会创建一块临时的空间,所以引用是一块临时空间的引用,而不是字面值的引用。

右值引用在以上的代码例子中的使用方式并不很常见。

右值引用作为函数参数

右值引用经常作为函数参数来使用。最有用的方式就是当函数重载的时候你希望对待左值和右值的时候有不同的表现。

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;
}

这会打印:

l-value reference to const
r-value reference

如你所见,当传入一个左值的时候,重载函数解析到了左值引用的版本。当传入右值的时候,重载函数解析到了右值引用的版本(这是比左值常引用更好的一个办法)

为什么你会想要这样做呢?我们将会在下节课中详细讨论,不得不说,这是移动语义的重要部分。

一个有趣的笔记:

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

事实上,调用的是左值版本的函数!尽管变量 ref 有一个整型的右值引用,但是它本身实际上是一个左值(所有的命名变量都是如此)。混淆的原因是右值这个词在不同的上下文中使用。你可以这样想:命名的对象是左值,匿名的对象是右值。命名对象和匿名对象不依赖于它是左值或者右值。或者,这样说,如果右值引用被叫做任何其他的名字,这个疑惑就不会存在。

返回一个右值引用

你应该几乎不会返回一个右值引用,同样你也应该几乎不返回一个左值引用。在大多数情况下,你会返回一个悬空引用当引用对象离开函数的作用域时

Quiz time

1) 标记哪一个字母对应的语句编译失败:

int main()
{
 int x{};

 // l-value references
 int &ref1{ x }; // A
 int &ref2{ 5 }; // B

 const int &ref3{ x }; // C
 const int &ref4{ 5 }; // D

 // r-value references
 int &&ref5{ x }; // E
 int &&ref6{ 5 }; // F

 const int &&ref7{ x }; // G
 const int &&ref8{ 5 }; // H

 return 0;
}

B, E, and G 会编译失败