【Effective Modern C++】第五章 右值引用、移动语义和完美转发:24. 区分万能引用和右值引用

T&&有两种身份,并非只有右值引用

  • 我们默认T&&是右值引用,但事实是,T&&在源码中可能有两种完全不同的含义;
  • 第一种:真正的右值引用 ------ 只能绑定到右值(临时对象、std::move后的对象),核心作用是识别可移动的对象;
  • 第二种:通用引用 (也叫转发引用)------ 本质是 "二重性引用",源码上写的是T&&,但可以表现为左值引用或右值引用,能绑定几乎任何东西(左值 / 右值、const/non-const、volatile 等);
  • 通用引用是一种 "抽象"(而非底层真相),底层是 "引用折叠",但这种抽象足够实用,能帮我们快速区分两种引用。

通用引用的判断:必须同时满足两个条件

1:存在类型推导
  • 要么是函数模板形参T(或模板参数)需要在函数调用时自动推导(排除调用者显式指定类型的边缘情况);
  • 要么是auto&& 声明的对象auto的类型需要在初始化时自动推导;
  • 没有类型推导,就不可能是通用引用。
2:声明形式必须是纯 T&&
  • 引用的声明必须严格是 "T&&",不能有任何额外修饰(比如constvolatile,或嵌套类型如std::vector<T>&&);
  • 这里的T只是模板参数名,换成其他名字(如MyTemplateType&&)也可以,关键是 "类型名 +&&" 的纯形式。
符合条件的通用引用示例
c++ 复制代码
// 1. 函数模板形参:有类型推导+纯T&& → 通用引用
template<typename T>
void f(T&& param);

// 2. auto&&声明:有类型推导+纯&& → 通用引用
auto&& var2 = var1; // var1是左值,var2就变成左值引用;var1是右值,就变成右值引用

// 3. 模板参数名非T,仍是通用引用(纯形式+推导)
template<typename MyTemplateType>
void someFunc(MyTemplateType&& param);

右值引用的判断:缺一个条件,就是右值引用

只要不满足通用引用的两个必备条件,哪怕源码是T&&,本质也是右值引用 ------ 只能绑定右值,不能绑定左值。

右值引用的示例
c++ 复制代码
// 1. 无类型推导:Widget&& 固定类型 → 右值引用
void f(Widget&& param);
Widget&& var1 = Widget(); // 无推导,直接绑定临时对象(右值)

// 2. 有类型推导,但不是纯T&&(带容器)→ 右值引用
template<typename T>
void f(std::vector<T>&& param); // 形式是std::vector<T>&&,不是纯T&&

// 3. 有类型推导,但不是纯T&&(带const)→ 右值引用
template<typename T>
void f(const T&& param); // 带const修饰,不是纯T&&
  • 向以上右值引用传递左值,会直接编译报错(右值引用不能绑定左值),比如std::vector<int> v; f(v);会报错。

模板中的T&&,不一定是通用引用

很多人误以为 "模板里的T&&都是通用引用",其实不然,核心看 "是否有独立的类型推导"。

错误示例:std::vector::push_back
c++ 复制代码
// vector的push_back成员函数
template<class T, class Allocator = allocator<T>>
class vector {
public:
    void push_back(T&& x); // 看似T&&,但不是通用引用
};
  • 原因:push_backT,是vector的模板参数 ------ 当vector<Widget> v实例化后,T已经确定是Widgetpush_back的声明就变成void push_back(Widget&& x)没有类型推导
  • 结论:push_backT&&右值引用 ,不能传递左值(比如Widget w; v.push_back(w);会报错)。
正确示例:std::vector::emplace_back
c++ 复制代码
template<class T, class Allocator = allocator<T>>
class vector {
public:
    template <class... Args>
    void emplace_back(Args&&... args); // Args&&是通用引用
};
  • 原因:emplace_backArgs,是独立于vector的模板参数 ,每次调用emplace_back时,Args都会重新推导;
  • 结论:Args&&满足 "类型推导 + 纯形式",是通用引用,可以绑定任何类型、任何值类别的实参。

auto&&:容易被忽略的通用引用

auto&&声明的对象,也是通用引用(满足 "类型推导 + 纯 &&"),虽然不如模板函数中的通用引用常见,但在 C++11/14 中高频出现。

示例
c++ 复制代码
// C++11:auto&&绑定左值/右值,会自动"变身"
auto&& var = 10;        // 绑定右值 → 变成int&&(右值引用)
auto&& var2 = var;      // 绑定左值 → 变成int&(左值引用)

// C++14:lambda表达式中的auto&&(高频用法)
auto timeFuncInvocation =
    [](auto&& func, auto&&... params) // func和params都是通用引用
    {
        // 转发参数,保留原始值类别(Item33详细讲解)
        std::forward<decltype(func)>(func)(
            std::forward<decltype(params)>(params)...
        );
    };
  • auto&&可以绑定任何对象(左值 / 右值),其最终类型(左值引用 / 右值引用),由初始化时的实参值类别决定 ------ 和模板函数中的通用引用 "变身" 逻辑完全一致。

通用引用的 "抽象" 本质

通用引用的概念,是一种 "抽象"(而非底层真相),底层实现是 "引用折叠"。但这种抽象非常实用:

  • 不用深究引用折叠的细节,就能快速区分T&&的身份;
  • 方便阅读代码(判断T&&是只绑定右值,还是能绑定任何对象);
  • 方便交流(明确告知合作者 "这里用的是通用引用,而非右值引用")。

总结

  1. T&&有两种身份:右值引用,或通用引用(转发引用);
  2. 通用引用的判断标准(缺一不可):① 存在类型推导(模板推导 /auto 推导);② 声明形式是纯T&&(无任何修饰);
  3. 不满足以上两个条件,T&&就是右值引用 ------ 只能绑定右值,不能绑定左值;
  4. 通用引用的 "变身" 特性:被右值初始化→变成右值引用;被左值初始化→变成左值引用;
  5. 模板中的T&&不一定是通用引用(如vector::push_back),关键看是否有 "独立的类型推导";
  6. auto&&也是通用引用,可绑定任何对象,常见于 C++14 lambda 表达式。

原著在线阅读地址

相关推荐
汉克老师5 小时前
GESP2025年3月认证C++五级( 第三部分编程题(1、平均分配))
c++·算法·贪心算法·排序·gesp5级·gesp五级
智者知已应修善业8 小时前
【51单片机2个按键控制流水灯运行与暂停】2023-9-6
c++·经验分享·笔记·算法·51单片机
云泽80810 小时前
C++11 核心特性全解:列表初始化、右值引用与移动语义实战
开发语言·c++
AI进化营-智能译站11 小时前
ROS2 C++开发系列12-用多态与虚函数构建可扩展的ROS2机器人行为模块
开发语言·c++·ai·机器人
Morwit11 小时前
QML组件之间的通信方案(暴露子组件)
c++·qt·职场和发展
qeen8711 小时前
【数据结构】建堆的时间复杂度讨论与TOP-K问题
c语言·数据结构·c++·学习·
图码11 小时前
如何用多种方法判断字符串是否为回文?
开发语言·数据结构·c++·算法·阿里云·线性回归·数字雕刻
handler0111 小时前
Linux 内核剖析:进程优先级、上下文切换与 O(1) 调度算法
linux·运维·c语言·开发语言·c++·笔记·算法
zhouwy11311 小时前
Linux进程与线程编程详解
linux·c++
A7bert77712 小时前
【YOLOv8pose部署至RDK X5】模型训练→转换bin→Sunrise 5部署
c++·python·深度学习·yolo·目标检测