第2章:模板技术工具箱
对齐第一版第2章 Techniques:编译期断言、偏特化、局部类、Int2Type/Type2Type、类型选择、可转换与继承检测、type_info 封装、NullType/EmptyType 与 Type Traits。
学习目标
- 能解释 compile-time assertion、partial specialization 与 overload resolution 如何把错误和选择前移到编译期
- 能实现 Int2Type、Type2Type、Select、convertibility detector 与基础 Type Traits,并读懂它们的实例化路径
- 能比较书中 C++98 技法与
static_assert、std::type_traits、concepts 的现代等价物,保留设计意图而非机械复刻语法
机制总览
第2章:模板技术工具箱:机制路径
- 1
为什么需要一章 Techniques
Policy 设计提出“让类型表达设计决定”,但 2001 年的 C++ 没有 static assert 、variadic templates、 std::type traits 或 concepts。第 2 章 Techniques 因而建立一套微型语言:把整数包装成类型、把类型包装成值、用 …
- 2
Compile-Time Assertions
书中的 Compile-Time Assertions 通过“只有 true 特化存在”的模板制造 incomplete type 错误。现代 C++ 应直接用 static assert(condition, message) ,因为诊断稳定、作用域清晰,也不会依赖负数组等非标准技巧。
- 3
Partial Template Specializati…
Partial Template Specialization 为一类“类型形状”提供规则,例如任意 T 、 const T 或 Container 。primary template 是默认规则,partial specialization 比默认更具体,full specialization 则匹配一个确切参数集合。
章级决策实验
第2章:模板技术工具箱:机制与证据
切换《第2章:模板技术工具箱》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么需要一章 Techniques
Policy 设计提出“让类型表达设计决定”,但 2001 年的 C++ 没有 static assert 、variadic templates、 std::type traits 或 concepts。第 2 章 Techniques 因而建立一套微型语言:把整数包装成类型、把类型包装成值、用 …
可核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要一章 Techniques」的组合规则与扩展边界。
学完《第2章:模板技术工具箱》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第2章:模板技术工具箱:失效与核验
为什么需要一章 Techniques
典型失效
若只复制「为什么需要一章 Techniques」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要一章 Techniques」的组合规则与扩展边界。
Compile-Time Assertions
典型失效
若只复制「Compile-Time Assertions」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Compile-Time Assertions」的组合规则与扩展边界。
Partial Template Specializati…
典型失效
若只复制「Partial Template Specializati…」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。
核验证据
用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Partial Template Specializati…」的组合规则与扩展边界。
为什么需要一章 Techniques
Policy 设计提出“让类型表达设计决定”,但 2001 年的 C++ 没有 static_assert、variadic templates、std::type_traits 或 concepts。第 2 章 Techniques 因而建立一套微型语言:把整数包装成类型、把类型包装成值、用 specialization 选择规则、用 overload resolution 探测表达式,再把结果送入类生成。
这些技巧的历史语法有些已经过时,推理方法却仍是现代 generic library 的基础:什么信息能在 compile time 得到、怎样让错误路径不实例化、怎样把检测结果变成更好的接口。
Compile-Time Assertions
↡当编译期布尔条件为假时使程序形成清晰诊断,从而在模板实例化点阻止违反不变量的类型或常量。书中的 Compile-Time Assertions 通过“只有 true 特化存在”的模板制造 incomplete type 错误。现代 C++ 应直接用 static_assert(condition, message),因为诊断稳定、作用域清晰,也不会依赖负数组等非标准技巧。
template<class T>
struct BinaryPacket {
static_assert(std::is_trivially_copyable_v<T>,
"BinaryPacket payload must be trivially copyable");
static_assert(sizeof(T) <= 64, "payload exceeds inline budget");
T value;
};断言应描述 violated contract,而不是重复表达式。sizeof(T) == 4 failed 不如 “wire field requires exactly 32 bits”;前者告诉结果,后者告诉设计原因。对可重载 API,concept/constraint 往往比函数体内 static_assert 更好,因为不合格候选能在选择阶段被排除。
Partial Template Specialization
Partial Template Specialization 为一类“类型形状”提供规则,例如任意 T*、const T 或 Container<T, Alloc>。primary template 是默认规则,partial specialization 比默认更具体,full specialization 则匹配一个确切参数集合。
template<class T>
struct StorageTraits {
static constexpr bool indirect = false;
using Element = T;
};
template<class T>
struct StorageTraits<T*> {
static constexpr bool indirect = true;
using Element = T;
};StorageTraits<int*> 选择 pointer 特化,不执行运行时判断。function templates 不能 partial-specialize,通常改用 overload、class helper 或 tag dispatch。特化还必须位于允许的 namespace,且规则重叠时要确保存在唯一“最特化”候选。
Local Classes:靠近使用点的类型
Local Classes 定义在函数体内,能把辅助类型限制在最小作用域。书中利用 local class 作为 algorithm 的临时 adapter 或 visitor,使实现细节不污染 namespace;旧标准对 local class 作为 template argument 有诸多限制,现代 C++ 已放宽,并常由 lambda 替代。
↡定义在函数块作用域内、仅在该作用域可见的类型,用于封装一次性适配状态或行为。选择 local class 还是 lambda 要看是否需要命名 nested types、多组成员函数、显式 constructor 或重复引用类型。单个 callable 通常 lambda 更直接;复杂协议的局部 adapter 仍可用 local struct。不要把“局部”误解为对象一定在 stack,storage duration 由实例的创建方式决定。
Mapping Integral Constants to Types
普通函数 overload 依据参数类型,不依据某个运行时 int 的值。Mapping Integral Constants to Types 用 Int2Type<N> 把 compile-time integral constant 嵌入一个独特类型,使 Int2Type<0> 和 Int2Type<1> 进入不同 overload。
template<int N>
struct Int2Type { static constexpr int value = N; };
template<class T>
void destroy(T* value, Int2Type<1>) { value->~T(); }
template<class T>
void destroy(T*, Int2Type<0>) {}
template<class T>
void destroy(T* value) {
destroy(value, Int2Type<!std::is_trivially_destructible_v<T>>{});
}这里空对象通常被优化掉;重要的是错误分支无需对目标类型成立。现代 if constexpr 常能更清楚地表达二分路径,但 tag dispatch 仍适用于 overload customization、旧标准兼容和多层扩展点。
Type-to-Type Mapping
Type-to-Type Mapping 用 Type2Type<T> 把“要创建哪个类型”变成普通函数参数的类型。它解决 constructor 不能 virtual、函数不能只按 return type overload 的限制:调用者传一个空 tag,implementation 据 tag 选择创建逻辑。
例如 create(Type2Type<Widget>{}) 与 create(Type2Type<Gadget>{}) 可以拥有不同实现。现代 factory 也可写 create<Widget>(),但 Type2Type 在 visitor/customization-point 中仍能把目标类型随普通参数一起传递,并支持 ADL/overload extension。
Type Selection
Type Selection 根据 compile-time boolean 返回 Then 或 Else 类型;Loki 的 Select<flag, T, U>::Result 对应今天的 std::conditional_t<flag,T,U>。它不是在两个已经构造的对象间选择,而是在定义 class layout、parameter type 或 policy base 前选定一种类型。
先预测:若 Select<false, Valid, typename Broken<T>::type> 的 Broken<T> 本身在形成模板实参时就非法,Select 未必能“保护”它。lazy selection 要把可能失败的计算延迟进尚未实例化的 class/template 分支;这也是 std::conditional_t 与 if constexpr 经常被误用的边界。
Detecting Convertibility and Inheritance at Compile Time
书中通过重载两个 Test 函数:一个接受目标类型,返回小类型;fallback 接受 ...,返回大类型;再比较 sizeof,实现 Detecting Convertibility and Inheritance at Compile Time。若 From 可转换到 To,更精确 overload 胜出。
这个 detector 要处理 void、相同类型、private/ambiguous base、user-defined conversion 与 incomplete types 等边界。“可转换”也不等于“公开继承”:conversion operator 能制造非继承转换,private base 又可能使真实继承不可访问。现代代码使用 std::is_convertible_v 与 std::is_base_of_v,并按语义选择,而不是把二者混为一个 trait。
A Wrapper Around type_info
typeid(T) 返回的 std::type_info 不能复制,直接存进 associative container 不便。A Wrapper Around type_info(Loki TypeInfo)保存 const std::type_info*,提供 copy、equality 与 ordering,从而可作为 factory/dispatcher 的运行时 key。现代标准有 std::type_index 完成同一角色。
type_info::before/type_index 的 order 只服务进程内排序,不是跨运行、跨编译器稳定的 serialization ID。plugin protocol 若把 name() 写盘或过网络,会依赖 implementation-specific mangling;应另定义稳定 schema identifier。
NullType and EmptyType
NullType and EmptyType 都是空类,却承担不同语义。NullType 是类型链或递归生成的结束哨兵,表示“没有类型”;EmptyType 是可被继承/实例化的占位单元,表示“有一个无状态默认实现”。把二者混用会让 termination 与 behavior 的意图消失。
empty class 在 C++ 中作为独立对象仍有非零大小,以保证不同对象地址;作为 base/member 则可能通过 empty-base optimization 或 [[no_unique_address]] 不增加 layout。是否优化是 object-model 问题,不能只看类中“没有字段”就断言零字节。
Type Traits:从事实到设计选择
Type Traits 收集类型事实与相关类型:是否 pointer/POD、parameter type 应按值还是引用、去掉 const 后是什么、怎样销毁。书中的 TypeTraits<T> 通过多层 specialization 建立 taxonomy;现代 <type_traits> 提供标准、编译器支持的基础积木。
Trait 的危险是把平台猜测包装成真理,例如“所有 fundamental type 按值传最快”忽略 ABI、register width 与 optimization。traits 应表达语义事实或经过证明的策略;性能选择仍需 measurement,公共 API 还要优先考虑 lifetime 与可读性。
第一步:定义默认规则与例外形状
从 primary template 写 conservative default,再用 partial specialization 覆盖 pointer、cv/ref 或容器形状;检查规则是否重叠以及哪一个最特化。
小结
- compile-time assertion 把不变量失败前移,候选资格优先用 constraints,内部事实再断言
- partial specialization 对类型形状建立规则,function template 则常用 overload/tag helper
- local class 把一次性 adapter 限制在使用点,现代单一 callable 常由 lambda 替代
- Int2Type、Type2Type 和 Select 在值、类型、overload 与结构之间搭桥
- convertibility 与 inheritance 是不同关系,现代代码应选择对应标准 trait
- TypeInfo wrapper 让 RTTI key 可复制比较,但不提供跨运行稳定身份
- NullType 是递归终点,EmptyType 是存在的空单元;语义不可互换
- Type Traits 要把可证明事实送入设计,不应把未经测量的平台猜测伪装成普遍规则
练习
- 问题 1:为 serializer 设计 pointer 特化。 默认序列化值本身,pointer 路径先检查空再序列化 pointee;说明 primary/partial specialization 的职责和 ownership 边界。
- 问题 2:选择 Int2Type、if constexpr 或 runtime branch。 一个算法按 compile-time trait 决定是否调用 destructor,另一个按用户输入选择 codec,分别说明选择。
- 问题 3:审计一个 TypeInfo registry。 registry 用
typeid(T).name()写入缓存文件,并在下次启动恢复;指出错误并给出分层 ID 方案。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- compile-time assertion
- 编译期条件失败即拒绝实例并给出契约诊断的机制。
- partial specialization
- 为一族模板参数形状提供比主模板更具体实现的规则。
- local class
- 定义在函数块内、只服务局部适配协议的类型。
- Int2Type
- 把整数常量包装成独特类型以参与重载和特化。
- Type2Type
- 把目标类型包装成普通函数可接收的空标签。
- type selection
- 依据编译期条件产生一个结果类型的元函数。
- convertibility detection
- 不执行转换而在编译期判断 From 是否可形成 To 的检测。
- TypeInfo wrapper
- 把 type_info 引用包装为可复制比较的进程内类型键。
- NullType
- 类型级递归的终止哨兵。
- EmptyType
- 可实例化或继承的无状态占位类型。
- Type Traits
- 查询或变换类型事实并驱动实现选择的元函数集合。