条款 24:区分万能引用和右值引用
回忆左值(Lvalue)和右值(Rvalue)
左值(Lvalue)表示一个有名字、可寻址、在表达式结束后仍然存在的对象。其特点是:
- 可以出现在赋值语句的左边:x = 5;
- 可以取地址:&x
- 通常是变量、数组元素、对象成员、函数形参等
右值(Rvalue)表示一个临时值,通常在表达式中短暂存在,不能取地址。其特点是:
- 只能出现在赋值语句的右边:x = 5;
- 通常是字面量、临时对象、表达式结果
- 不具备持久性,生命周期短
- 可以移动(move),而不能复制(copy)
基于此, 我们可以得到右值引用 (Rvalue Reference): T&&. 这是一种新的引用类型,用 && 表示。它有一个关键特性:它只能绑定到右值(临时对象)。这使得我们可以在函数重载时区分传入的是左值还是右值:
1 | void foo(const MyClass& obj); // 版本 1: 接受左值 (const引用也可以接受右值,但这是题外话) |
更进一步地, 右值还可以继续划分: 右值 (rvalue) = 纯右值 (prvalue) + 将亡值 (xvalue)
其中纯右值是没有名字的临时值或返回非引用的函数, 而将亡值特指资源即将被销毁的对象,通常是右值引用表达式即std::move(obj)或者返回T&&类型的函数返回值.
万能引用(Universal Reference)/ 转发引用(Forwarding Reference)
万能引用(转发引用)是一种特殊的引用,它既可以绑定到左值,也可以绑定到右值。它在语法上看起来与右值引用完全相同(都是使用 &&),但它只在非常特定的上下文中出现,并且遵循一套完全不同的类型推导规则。
一个参数要成为“万能引用”,必须同时满足以下两个条件:
- 必须涉及模板类型推导或者auto类型推导: 这个引用必须依赖于一个正在被推导的模板参数T或者auto。
- 参数的声明形式必须是 T&&: 必须是这个精确的形式。不能有 const,也不能是其他任何形式。
1 | void f(Widget&& param); // 没有类型推导,是右值引用 |
万能引用如何工作:特殊的类型推导与引用折叠
万能引用的“魔力”来自于它在模板类型推导中使用的特殊规则,以及 C++ 的引用折叠(Reference Collapsing)规则。
当我们使用
template<typename T> void func(T&& param)
时:
- 情况一:传递一个左值
假设我们有一个左值变量: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 上。
- 情况二:传递一个右值
假设我们传递一个临时对象(右值):func(MyClass());
类型推导(关键规则): 当一个右值(类型为 A)被传递给一个 T&& 形式的万能引用时,模板参数 T 被推导为 A (一个普通的、非引用的类型)。本例中,T 被推导为 MyClass。
实例化参数类型: 编译器将 T (即 MyClass) 替换回参数声明 T&& 中,得到:(MyClass) &&,即 MyClass&&。(注意这里不需要发生折叠)
最终函数签名: func 被实例化的版本是:
void func(MyClass&& param);函数变成了一个接受右值引用的函数,它成功地绑定到了我们传入的右值临时对象上。