万能引用、引用折叠与完美转发
总览:一条主线串起所有点
目标:让"传进来的原始参数"尽可能少经过临时对象,直接就地构造到目标里。所有规则都是为这件事服务的。
一、两块基石
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 高效的同一套原理。