思考: C++ 万能引用与完美转发

万能引用、引用折叠与完美转发

总览:一条主线串起所有点

目标:让"传进来的原始参数"尽可能少经过临时对象,直接就地构造到目标里。所有规则都是为这件事服务的。


一、两块基石

1. 类型 ≠ 值类别

一个表达式同时有两个属性:

表达式 类型 值类别
s1 string 左值(lvalue)
string("x") string 右值(rvalue)

s1(左值)和 string("x")(右值)类型都是 string,只是值类别不同。

2. 引用折叠(标准规定)

推导结果 折叠为
T& & T&
T& && T&
T&& & T&
T&& && T&&

只要出现一个 &,结果就是左值引用。


二、万能引用为什么要"再写一个函数模板 U"

规则:*X&& 是万能引用,当且仅当 X 是该函数自己、会被推导的模板参数。

写法 说明
list<T>::push_back(T&&) T 是类模板参数,在 list<string> 实例化时已定死 → 普通右值引用
list<T>::push_back(U&&) 套函数模板 U 由函数自己推导 → 万能引用

推导 + 折叠效果:

实参 左值 s1 右值 string(...)
U 推导为 string& string
U&& 折叠为 string& string&&

结论: 类模板参数 T 在 list<string> 时定死,永远变不成万能引用;必须让函数自己推导 U,才能左右值通吃 + 用 forward<U> 保留值类别。


三、ListNode 两种写法:为什么 1 次 vs 2 次构造

以 new Node("literal") 为例(字面量类型 const char[N]):

写法 A(推导):template<class U> ListNode(U&&)

复制代码
字面量原样进来(U = const char(&)[N])
  → _data(forward<U>(data)) 直接调 string(const char*)
  ⇒ 1 次构造

写法 B(写死):ListNode(const T&) / ListNode(T&&)

复制代码
字面量先隐式转成临时 string(进门前)
  ⇒ 构造 1(转换)
  → _data(data) / _data(move(data)) 再拷/移
  ⇒ 构造 2(拷贝/移动)

根因

写法 B 的形参类型已经是 string,字面量必须先在门口变成 string 临时;写法 A 的形参是"推导出的原始类型",字面量原封不动抵达 _data 的构造函数。

差异只在参数类型 ≠ 元素类型时出现 ;若本来传 string,两种写法都是 1 次拷/移,打平。


四、push_back vs emplace_back:同一份开销的两个层次

emplace_back = 把 insert / ListNode 也做成变参转发 ,让"原始参数"一路直达 _data 构造:

cpp 复制代码
template<class... Args> void emplace_back(Args&&... args)
{ insert(end(), std::forward<Args>(args)...); }

template<class... Args> iterator insert(iterator pos, Args&&... args)
{ ... new Node(std::forward<Args>(args)...); ... }

template<class... Args> ListNode(Args&&... args)
    : _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...) {}

对比 lt.push_back("...") 和 lt.emplace_back("..."):

调用 传字面量 "..." 传 string 左值 传 std::move(s)
push_back 门口先转临时 string(2 次构造)→ 再拷/移 1 次拷贝 1 次移动
emplace_back 原样直达 _data(1 次构造) 1 次拷贝 1 次移动

层层对应

层次 push_back vs emplace_back ListNode(const T&) vs ListNode(U&&)
转发层数 push_back 只转发一层,emplace_back 变参转发到底 写死类型在门口转换,推导类型原样进内部
效果 字面量先变临时对象 字面量先变临时对象
优势 emplace_back 少一次构造 推导写法少一次构造

emplace_back 的效率优势 = 上面第三节那个"字面量先变临时"的开销,被变参转发消除掉了。


五、一张总表

场景 收左值 收右值 传字面量 保留值类别(左右值区分)
T&&(写死,普通右值引用) ✗ 只收右值 ✓ ✗ 先转临时(2 次构造) 部分(左值直接绑不上)
U&&(函数模板,万能引用) ✓ U=T& ✓ U=T ✓ 原样直达(1 次构造) ✓ 配合 forward<U>
push_back(一层转发) ✓ 拷贝 ✓ 移动 ✗ 先转临时 ✓
emplace_back(变参转发到底) ✓ 拷贝 ✓ 移动 ✓ 1 次就地构造 ✓

六、一句话串起来

类模板 T 在实例化时定死,T&& 只是普通右值引用,且"写死的类型"会逼实参在门口先造临时对象;所以要再写一个函数模板 U / Args,靠推导 + 引用折叠得到万能引用,再用 forward 原样把原始参数送到 _data 的构造函数------这就是 ListNode 省一次构造、emplace_back 比 push_back 高效的同一套原理。

相关推荐
FlightYe25 分钟前
视界原理之3D视频(四):裸眼、眼镜和头显
android·linux·c++·3d·ffmpeg·音视频
GlobalInfo1 小时前
2026年全球机器人用超平执行器市场研究报告:市场规模达7.51亿美元,人形机器人商业化与轻量化运动控制需求推动行业增长
大数据·人工智能·数码相机·microsoft·机器人
by209992 小时前
C++ 智能指针的所有权与使用
开发语言·c++·经验分享
Rover Ramble2 小时前
C++ 用互斥锁实现读写锁
开发语言·c++
你怎么知道我是队长2 小时前
C++零基础入门第2周:数据类型、运算符与表达式完全指南(附20段可运行代码+避坑清单)
c++
say_fall3 小时前
【Linux系统编程】Ext 文件系统:磁盘与存储、文件与目录、现代计算机究竟如何寻找文件
linux·运维·服务器·数据库·c++
雪落漂泊3 小时前
C++算法 滑动窗口篇(下)
开发语言·c++
共享家95273 小时前
408 操作系统第一章
服务器·数据库·windows
Sakura_j_h_5 小时前
GitHub Actions 打包 Windows exe:中文编码与产物上传
windows·ci/cd·打包发布·github·pyinstaller·github actions
你怎么知道我是队长6 小时前
C++零基础第3周:if/else、switch、for/while/do-while 控制流完全指南(附20段可运行代码+避坑清单)
java·开发语言·c++