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