在第 12 章中,我们引入了值类别
如果你对左值与右值已经模糊,现在正是复习的好时机,因为本章将频繁使用它们。
左值引用回顾
在 C++11 之前,C++ 只有一种引用,因此直接称为“引用”。但从 C++11 起,它被称为左值引用。左值引用只能以可修改的左值初始化。
| 左值引用 | 可初始化的类型 | 能否修改 | 
|---|---|---|
| 可修改左值 | 可以 | 可以 | 
| 不可修改左值 | 不可以 | 不可以 | 
| 右值 | 不可以 | 不可以 | 
const 左值引用既可绑定到可修改左值,也可绑定到不可修改左值和右值;但通过这些引用无法修改原值。
| const左值引用 | 可初始化的类型 | 能否修改 | 
|---|---|---|
| 可修改左值 | 可以 | 不可以 | 
| 不可修改左值 | 可以 | 不可以 | 
| 右值 | 可以 | 不可以 | 
因此,const 左值引用尤为实用:在无需拷贝实参的前提下,可把任何类型(左值或右值)的实参传递给函数。
右值引用
C++11 新增了一种引用类型,称为右值引用。右值引用只能以右值初始化。与左值引用(单个 &)不同,右值引用使用双 &&:
int x{ 5 };
int&  lref{ x };   // 左值引用,以左值 x 初始化
int&& rref{ 5 };   // 右值引用,以右值 5 初始化
右值引用不能以左值初始化。
| 右值引用 | 可初始化的类型 | 能否修改 | 
|---|---|---|
| 可修改左值 | 不可以 | 不可以 | 
| 不可修改左值 | 不可以 | 不可以 | 
| 右值 | 可以 | 可以 | 
const 右值引用只能绑定到右值,且不可修改其绑定对象。
| const右值引用 | 可初始化的类型 | 能否修改 | 
|---|---|---|
| 可修改左值 | 不可以 | 不可以 | 
| 不可修改左值 | 不可以 | 不可以 | 
| 右值 | 可以 | 不可以 | 
右值引用具备两个实用特性:
- 延长绑定对象的生命周期,直至该引用自身消亡(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 } }; // 右值引用绑定到临时 Fraction
    // operator<< 的形参 f1 引用该临时对象,无需拷贝
    std::cout << rref << '\n';
    return 0;
} // rref(及其绑定的临时 Fraction)在此离开作用域
输出:
3/5
Fraction(3, 5) 作为匿名对象,本应在其定义所在表达式结束时销毁;但因其被用于初始化右值引用,其生命周期被延长至块末,故可安全通过 rref 打印其值。
示例二(稍反直觉):
#include <iostream>
int main()
{
    int&& rref{ 5 }; // 用字面量 5 初始化,编译器会为之创建临时 int
    rref = 10;
    std::cout << rref << '\n';
    return 0;
}
输出:
10
虽然用字面量初始化右值引用并修改其值看似怪异,但编译器会为字面量 5 创建临时对象,右值引用实际绑定的是该临时对象,而非字面量本身。
上述用法在实践中并不常见。
右值引用作为函数形参
右值引用更常用于函数形参的重载,以区分左值与右值实参的不同处理。
#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;
}
输出:
l-value reference to const: 5
r-value reference: 5
可见,传入左值时重载决议选择左值引用版本;传入右值时选择右值引用版本(相比 const 左值引用,右值引用被视为更佳匹配)。
为何需要这样做?下一节将详细讨论;简言之,这是实现移动语义的关键一环。
右值引用变量本身是左值
考虑以下片段:
int&& ref{ 5 };
fun(ref);
你以为会调用 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{};
    // 左值引用
    int& ref1{ x };   // A
    int& ref2{ 5 };   // B
    const int& ref3{ x }; // C
    const int& ref4{ 5 }; // D
    // 右值引用
    int&& ref5{ x };  // E
    int&& ref6{ 5 };  // F
    const int&& ref7{ x }; // G
    const int&& ref8{ 5 }; // H
    return 0;
}
