思考: 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 高效的同一套原理。

相关推荐
余额瞒着我当琳1 小时前
C++算法学习篇5--二分查找法
c++·学习·算法
蒸蒸yyyyzwd2 小时前
秋招学习笔记 Day 64 之准备系统软件开发工程师岗位笔记
c++·八股
奇树谦2 小时前
LRU 缓存原理与 C++ 实践示例
开发语言·c++·缓存
多弗朗皮卡丘2 小时前
C++ 红黑树
开发语言·c++·红黑树
howdoyoudo2026062 小时前
AI基建融资信用分层:谁被挤出GPU抵押市场?
大数据·人工智能·安全·microsoft·ai·语言模型
曼巴UE53 小时前
UE 属性同步
网络·c++·ue5
小趴菜一只3 小时前
SIEM-wazuh安装过程
运维·windows·安全·网络安全
无忧.芙桃3 小时前
图的精髓:邻接矩阵与遍历全解析
c++·算法·面试·深度优先·广度优先
jimy13 小时前
标准的动态类型定义与“多态”无关,非多态对象同样会出现动态类型不同于静态类型的情形
开发语言·c++