22-2 R值引用
在第12章中,我们介绍了值类别的概念(12.2节——值类别(左值与右值)),这是表达式的一种属性,有助于确定表达式解析为值、函数还是对象。我们还介绍了左值和右值,以便讨论左值引用。
若您对左值与右值的概念尚不清晰,建议趁此机会重新温习该主题,因为本章将大量涉及相关内容。
左值引用回顾
在C++11之前,C++中仅存在一种引用类型,因此统称为“引用”。然而在C++11中,它被称为左值引用。左值引用只能通过可修改的左值进行初始化。
| L-value reference | Can be initialized with | Can modify |
|---|---|---|
| Modifiable l-values | Yes | Yes |
| Non-modifiable l-values | No | No |
| R-values | No | No |
对 const 对象的左值引用既可用可修改的左值和右值初始化,也可用不可修改的左值和右值初始化。但这些值不能被修改。
| 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 |
const 对象的左值引用尤其有用,因为它们允许我们将任何类型的参数(左值或右值)传递给函数,而无需复制该参数。
右值引用
C++11新增了一种名为右值引用的引用类型。右值引用专为仅能用右值初始化而设计。左值引用使用单个&符号创建,而右值引用则使用双&符号创建:
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 |
右值引用具有两个有用的特性。首先,右值引用将初始化对象的生命周期延长至右值引用的生命周期(指向 const 对象的左值引用也能实现此效果)。其次,非 const 右值引用允许你修改右值!
让我们通过几个示例来了解:
#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 } }; // 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
该程序输出:

作为匿名对象,Fraction(3, 5)通常会在定义它的表达式结束时超出作用域。但由于我们用它初始化了一个右值引用,其生命周期被延长至代码块结束。随后我们就能使用该右值引用打印分数的值。
现在来看一个不太直观的例子:
#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;
}
该程序输出:

虽然用字面量初始化右值引用后还能修改该值看似奇怪,但当用字面量初始化右值引用时,系统会从字面量构造一个临时对象,因此该引用实际指向的是临时对象而非字面量本身。
上述两种用法在实际编程中都较为罕见。
右值引用作为函数参数
右值引用更常作为函数参数使用。这在函数重载时尤为有用,当你希望左值和右值参数具有不同行为时。
#include <iostream>
void fun(const int& lref) // l-value arguments will select this function
{
std::cout << "l-value reference to const: " << lref << '\n';
}
void fun(int&& rref) // r-value arguments will select this function
{
std::cout << "r-value reference: " << rref << '\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左值引用,这种匹配被认为更优)。
为何需要这样做?我们将在下一课中深入探讨。毋庸置疑,这是移动语义的重要组成部分。
右值引用变量是左值
考虑以下代码片段:
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&&),因为右值引用无法与左值绑定。
返回右值引用
你几乎不应返回右值引用,原因与你几乎不应返回左值引用相同。在大多数情况下,当被引用的对象在函数结束时超出作用域时,你最终会返回一个悬空引用。
测验时间
问题 #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和G无法编译。
重申绑定组合规则:
* 非常量左值引用只能绑定到非常量左值。
* 常量左值引用可绑定到非常量左值、常量左值和右值。
* 右值引用只能绑定到右值。
x是常量左值,因此我们可以将其绑定到非常量左值引用(A)和常量左值引用(C)。我们不能将非常量左值引用绑定到右值(B)。
5 是右值,因此我们可以将其绑定为常左值引用(D)和右值引用(F & H)。我们不能将右值引用绑定为左值(E & G)。

浙公网安备 33010602011771号