第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_assertstd::type_traits、concepts 的现代等价物,保留设计意图而非机械复刻语法

机制总览

第2章:模板技术工具箱:机制路径

  1. 1

    为什么需要一章 Techniques

    Policy 设计提出“让类型表达设计决定”,但 2001 年的 C++ 没有 static assert 、variadic templates、 std::type traits 或 concepts。第 2 章 Techniques 因而建立一套微型语言:把整数包装成类型、把类型包装成值、用 …

  2. 2

    Compile-Time Assertions

    书中的 Compile-Time Assertions 通过“只有 true 特化存在”的模板制造 incomplete type 错误。现代 C++ 应直接用 static assert(condition, message) ,因为诊断稳定、作用域清晰,也不会依赖负数组等非标准技巧。

  3. 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 TContainer<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 TypesInt2Type<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 MappingType2Type<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 返回 ThenElse 类型;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_tif 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_vstd::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 与可读性。

分步1 / 3

第一步:定义默认规则与例外形状

从 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. 问题 1:为 serializer 设计 pointer 特化。 默认序列化值本身,pointer 路径先检查空再序列化 pointee;说明 primary/partial specialization 的职责和 ownership 边界。
  1. 问题 2:选择 Int2Type、if constexpr 或 runtime branch。 一个算法按 compile-time trait 决定是否调用 destructor,另一个按用户输入选择 codec,分别说明选择。
  1. 问题 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
查询或变换类型事实并驱动实现选择的元函数集合。

资料与写作方式声明

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

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

讨论

评论区加载中…