Item 1:理解 template type deduction
对齐 Effective Modern C++ Item 1:从 ParamType 的 reference/pointer、universal reference 与 by-value 三种形态推导 T,区分 top-level/low-level cv,并保留或观察 array/function decay。
学习目标
- 能区分 ParamType 是普通 reference/pointer、universal reference 或 by-value 时的 template type deduction 规则
- 能推导 lvalue/rvalue、top-level/low-level const 对 T 与最终 ParamType 的影响
- 能复现 array/function 按值 decay 与按引用保留类型,并实现 compile-time array extent helper
从两个同时被推导的类型开始
template<class T>
void inspect(ParamType param);
inspect(expression);compiler 不只推导 T,还根据 T 与源码中的修饰组合出 ParamType。相同 expression 传给 T&、T&& 或 T 会得到不同结果。
Item 1 的原则是 Understand template type deduction(理解模板类型推导)。先分类 ParamType,再处理 expression。
先预测每个 call 的 T 和 ParamType,不要只说“传入 int”。T 可以包含 const,甚至可被推导为 reference。
Case 1:ParamType 是普通 reference 或 pointer
原书把这一类称为 ParamType is a Reference or Pointer(ParamType 是引用或指针参数),但明确排除 universal reference;其共同点是模式本身已固定 reference/pointer 方向。
template<class T> void byRef(T& value);
template<class T> void byConstRef(const T& value);
template<class T> void byPtr(T* value);
int x = 0;
const int cx = x;
const int& rx = x;
byRef(x); // T=int, ParamType=int&
byRef(cx); // T=const int, ParamType=const int&
byRef(rx); // T=const int, ParamType=const int&
byConstRef(x); // T=int, ParamType=const int&
byPtr(&cx); // T=const int, ParamType=const int*rx 的 declared type 是 const int reference,但 expression 参与推导时先去掉 reference 层;const 仍保留在 T。
↡直接修饰对象本身、对按值复制后的新对象不再生效的 const/volatile。 ↡修饰 pointer 指向对象或成员所指对象、属于复合类型内部的 const/volatile。pointer case 保留 pointee const:按值复制 pointer 不允许修改它指向的 const object。
Case 2:ParamType 是 universal reference
只有形如 T&& 且 T 需要推导时,它才是 universal/forwarding reference。
template<class T>
void relay(T&& value);
int x = 0;
const int cx = x;
relay(x); // T=int&, ParamType=int&
relay(cx); // T=const int&, ParamType=const int&
relay(27); // T=int, ParamType=int&&lvalue 是特殊规则:T 被推导为 lvalue reference。随后 reference collapsing 把 T reference 与源码 && 合并成 lvalue reference。
这正是 perfect forwarding 能保留调用者值类别的基础。
Case 3:ParamType 按值
template<class T>
void byValue(T value);
const int cx = 7;
const int& rx = cx;
const char* const ptr = "data";
byValue(cx); // T=int
byValue(rx); // T=int
byValue(ptr); // T=const char*reference 和 top-level const 被丢弃,因为 value 是独立参数对象;但 ptr 指向 char 的 low-level const 保留,参数仍不能通过 pointer 修改字符。
↡按值推导时移除 expression 最外层 const/volatile,而保留复合类型内部限定。const pointer 的两层不要混淆
const char* const 有两层:右侧 const 约束 pointer 本身,左侧 const 约束 pointee。by-value 复制 pointer 后,新 pointer 可改指向,所以 top-level const 消失;它仍指向 const char,所以 low-level const 保留。
写类型推导表时把“pointer const”和“pointee const”分列,避免无意丢弃只读 contract。
arrays:按值会 decay
const char name[] = "modern cpp";
template<class T> void valueParam(T value);
template<class T> void refParam(T& value);
valueParam(name); // T=const char*
refParam(name); // T=const char[11]reference 参数可绑定完整 array type,因此保留 extent。
↡数组类型中编译期已知的元素数量。template<class T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
static_assert(arraySize(name) == 11);这是把“不 decay”转化为可用 compile-time information 的实际技巧。
functions 也有相同分岔
void process(int value);
valueParam(process); // T=void(*)(int)
refParam(process); // T=void(int)按 reference 推导可保留 function type。generic wrapper 若需要精确 callable 类型和值类别,应优先 reference/forwarding 模型,而非先 decay。
推导发生在 conversion 之前
template deduction 依据 expression 和 ParamType pattern,不会先执行任意用户定义 conversion 再猜 T。
↡先从参数模式形成 specialization,再对已确定函数参数执行允许转换的顺序。这解释了许多“明明可转换却推导失败”的调用。先区分 deduction failure 和 overload conversion failure。
推导规则会直接改变 API contract
选择 by-value、reference 或 universal reference 不是语法偏好。by-value 明确创建独立参数对象,因此有意移除 top-level cv,并让 array arguments 与 function arguments decay;普通 reference 保留 identity、cv 与原始 array/function type;universal reference 还保留调用者 value category,通常服务 forwarding boundary。
若算法只读取对象且不保存,const reference 往往最清楚;小型值或本来就要复制的参数可按值;只有确实需要把 lvalue/rvalue 行为转交下游时才使用 universal reference。过度使用 forwarding reference 会扩大 overload set、产生意外匹配,并把 diagnostics 推迟到深层实例化。
推导完成后仍要审查 lifetime:reference 参数不会延长临时对象到函数调用之外,forward 到异步任务或成员存储时尤其危险。类型推导回答“参数是什么”,不自动回答“参数能活多久”。
↡根据复制、identity、cv、value category 和 lifetime 需求选择参数模式的接口决策。一套纸上推导流程
- 写出 expression 的 declared type、cv 和 lvalue/rvalue category。
- 判断 ParamType 属于普通 reference/pointer、universal reference 或 by-value。
- 应用该 case 的 reference/cv 规则得到 T。
- 把 T 代回 ParamType,执行 reference collapsing。
- 单独检查 array/function 是否因 by-value decay。
- 用 compiler diagnostic/static assertion 验证,不用 IDE 简写当唯一证据。
小结
- 普通 reference/pointer 推导忽略 expression reference 层但保留匹配所需 cv
- universal reference 接收 lvalue 时 T 被推导为 lvalue reference,rvalue 时 T 为非引用
- by-value 丢弃 reference 和 top-level cv,但保留 pointer pointee 的 low-level cv
- arrays/functions 按值 decay,按 reference 可保留原始类型和 array extent
- 纸上先推 T,再还原 ParamType,并用 static assertions 验证
名词解释
本章出现的专业名词,用大白话再讲一遍。
- template type deduction
由实参和参数模式推导 T。
- ParamType pattern
包含 T 的完整参数类型形式。
- reference stripping for matching
普通引用匹配时先去实参引用层。
- top-level cv
- 直接修饰对象本身的 cv。
- low-level cv
- 复合类型内部对象的 cv。
- universal reference
可绑定左值右值的推导 T 引用。
- lvalue-reference deduction
左值使 T 推导为左值引用。
- reference collapsing
- 多层引用合并规则。
- by-value deduction
参数为独立复制对象的推导。
- top-level cv removal
- 按值丢弃最外层 cv。
- pointer cv layers
pointer 与 pointee 两层限定。
- array-to-pointer decay
数组按值转换为首元素 pointer。
- array extent
- 数组编译期元素数量。
- function-to-pointer decay
函数名按值转换为函数 pointer。
- deduction-before-conversion
先推导 specialization 再执行转换。
- deduction-driven parameter design
按推导和生命周期需求选择参数模式。
- deduction worksheet
- 分步记录类型推导的表。
练习
- 问题 1:推导三种 calls。 const int reference 分别传给 T reference、T double ampersand 和 T 参数。
- 问题 2:分析 const char pointer 的两层 const。 传入 by-value template 后哪些消失?
- 问题 3:实现只接受原生数组并返回元素数的 helper。 pointer 不能误通过。