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。

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 修改字符。

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。

这解释了许多“明明可转换却推导失败”的调用。先区分 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 到异步任务或成员存储时尤其危险。类型推导回答“参数是什么”,不自动回答“参数能活多久”。

一套纸上推导流程

  1. 写出 expression 的 declared type、cv 和 lvalue/rvalue category。
  2. 判断 ParamType 属于普通 reference/pointer、universal reference 或 by-value。
  3. 应用该 case 的 reference/cv 规则得到 T。
  4. 把 T 代回 ParamType,执行 reference collapsing。
  5. 单独检查 array/function 是否因 by-value decay。
  6. 用 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 验证

资料与写作方式声明

本章以Effective Modern C++, Item 1权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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. 问题 1:推导三种 calls。 const int reference 分别传给 T reference、T double ampersand 和 T 参数。
  1. 问题 2:分析 const char pointer 的两层 const。 传入 by-value template 后哪些消失?
  1. 问题 3:实现只接受原生数组并返回元素数的 helper。 pointer 不能误通过。

讨论

评论区加载中…