右值引用

在第 12 章中,我们引入了值类别这一概念。值类别是表达式的一种属性,用于判定表达式究竟解析为一个值、一个函数还是一个对象。为了讨论左值引用,我们也引入了左值(l-value)与右值(r-value)。

如果你对左值与右值已经模糊,现在正是复习的好时机,因为本章将频繁使用它们。

左值引用回顾

在 C++11 之前,C++ 只有一种引用,因此直接称为“引用”。但从 C++11 起,它被称为左值引用。左值引用只能以可修改的左值初始化。

左值引用可初始化的类型能否修改
可修改左值可以可以
不可修改左值不可以不可以
右值不可以不可以

const 左值引用既可绑定到可修改左值,也可绑定到不可修改左值和右值;但通过这些引用无法修改原值。

const 左值引用可初始化的类型能否修改
可修改左值可以不可以
不可修改左值可以不可以
右值可以不可以

因此,const 左值引用尤为实用:在无需拷贝实参的前提下,可把任何类型(左值或右值)的实参传递给函数。


右值引用

C++11 新增了一种引用类型,称为右值引用。右值引用只能以右值初始化。与左值引用(单个 &)不同,右值引用使用&&

int x{ 5 };
int&  lref{ x };   // 左值引用,以左值 x 初始化
int&& rref{ 5 };   // 右值引用,以右值 5 初始化

右值引用不能以左值初始化。

右值引用可初始化的类型能否修改
可修改左值不可以不可以
不可修改左值不可以不可以
右值可以可以

const 右值引用只能绑定到右值,且不可修改其绑定对象。

const 右值引用可初始化的类型能否修改
可修改左值不可以不可以
不可修改左值不可以不可以
右值可以不可以

右值引用具备两个实用特性:

  1. 延长绑定对象的生命周期,直至该引用自身消亡(const 左值引用亦可做到)。
  2. 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;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"