条款 24:区分万能引用和右值引用

ZaynPei Lv6

回忆左值(Lvalue)和右值(Rvalue)

左值(Lvalue)表示一个有名字、可寻址、在表达式结束后仍然存在的对象。其特点是:

  • 可以出现在赋值语句的左边:x = 5;
  • 可以取地址:&x
  • 通常是变量、数组元素、对象成员、函数形参等

右值(Rvalue)表示一个临时值,通常在表达式中短暂存在,不能取地址。其特点是:

  • 只能出现在赋值语句的右边:x = 5;
  • 通常是字面量、临时对象、表达式结果
  • 不具备持久性,生命周期短
  • 可以移动(move),而不能复制(copy)

基于此, 我们可以得到右值引用 (Rvalue Reference): T&&. 这是一种新的引用类型,用 && 表示。它有一个关键特性:它只能绑定到右值(临时对象)。这使得我们可以在函数重载时区分传入的是左值还是右值:

1
2
void foo(const MyClass& obj); // 版本 1: 接受左值 (const引用也可以接受右值,但这是题外话)
void foo(MyClass&& obj); // 版本 2: (C++11 新增) 只接受右值

更进一步地, 右值还可以继续划分: 右值 (rvalue) = 纯右值 (prvalue) + 将亡值 (xvalue)

其中纯右值是没有名字的临时值返回非引用的函数, 而将亡值特指资源即将被销毁的对象,通常是右值引用表达式即std::move(obj)或者返回T&&类型的函数返回值.

万能引用(Universal Reference)/ 转发引用(Forwarding Reference)

万能引用(转发引用)是一种特殊的引用,它既可以绑定到左值,也可以绑定到右值。它在语法上看起来与右值引用完全相同(都是使用 &&),但它只在非常特定的上下文中出现,并且遵循一套完全不同的类型推导规则。

一个参数要成为“万能引用”,必须同时满足以下两个条件:

  • 必须涉及模板类型推导或者auto类型推导: 这个引用必须依赖于一个正在被推导的模板参数T或者auto。
  • 参数的声明形式必须是 T&&: 必须是这个精确的形式。不能有 const,也不能是其他任何形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void f(Widget&& param);   // 没有类型推导,是右值引用

Widget&& var1 = Widget(); // 右值引用

auto&& var2 = var1; // auto类型推导, 是万能引用

template<typename T>
void f(std::vector<T>&& param); // 非精确形式, 是右值引用
std: :vector<int> v;
f(v); // 此时会错误!不能给右值引用绑定左值

template<typename T>
void f(const T&& param); // 非精确形式, 是个右值引用

template<typename T>
void f(T&& param); // 模板参数推导+精确形式, 是万能引用
Widget w;
f(w); // 传递左值, param为左值引用Widget&
f (std::move(w)); // 传递右值, param为右值引用Widget&&

万能引用如何工作:特殊的类型推导与引用折叠

万能引用的“魔力”来自于它在模板类型推导中使用的特殊规则,以及 C++ 的引用折叠(Reference Collapsing)规则。

当我们使用 template<typename T> void func(T&& param) 时:

  1. 情况一:传递一个左值

假设我们有一个左值变量:MyClass widget; 我们调用:func(widget);

  • 类型推导(关键规则): 当一个左值(类型为 A)被传递给一个 T&& 形式的万能引用时,模板参数 T 被推导为 A& (一个左值引用)。在本例中,T 被推导为 MyClass&。

  • 实例化参数类型: 编译器将 T (即 MyClass&) 替换回参数声明 T&& 中,得到(MyClass&) &&

  • 引用折叠: C++ 不允许“引用的引用”。编译器使用“引用折叠”规则来简化它。折叠规则是(简单来说):只要有 & 出现,最终就折叠为 &。只有当两者都是 && 时,才折叠为 &&

    • A& & -> A&

    • A& && -> A& (我们命中的规则)

    • A&& & -> A&

    • A&& && -> A&&

  • 最终函数签名: 折叠后,func 被实例化的版本是:void func(MyClass& param);, 结果是函数变成了一个接受左值引用的函数,它成功地绑定到了我们传入的左值 widget 上。

  1. 情况二:传递一个右值

假设我们传递一个临时对象(右值):func(MyClass());

  • 类型推导(关键规则): 当一个右值(类型为 A)被传递给一个 T&& 形式的万能引用时,模板参数 T 被推导为 A (一个普通的、非引用的类型)。本例中,T 被推导为 MyClass。

  • 实例化参数类型: 编译器将 T (即 MyClass) 替换回参数声明 T&& 中,得到:(MyClass) &&,即 MyClass&&。(注意这里不需要发生折叠)

  • 最终函数签名: func 被实例化的版本是:void func(MyClass&& param);函数变成了一个接受右值引用的函数,它成功地绑定到了我们传入的右值临时对象上。