第3章:Typelists

对齐第一版第3章 Typelists:类型链定义与线性化、Length/TypeAt/IndexOf、Append/Erase/NoDuplicates/Replace、继承偏序,以及 scatter/linear hierarchy 与 tuple 类生成。

学习目标

  • 能解释 the need for typelists,并手工展开 Head/Tail/NullType 的递归表示与线性化构造
  • 能实现 Length、TypeAt、IndexOf、Append、Erase、NoDuplicates、Replace 与 inheritance ordering,说明每个终止特化
  • 能设计 typelist 驱动的 scatter hierarchy、tuple class 与 linear hierarchy,并判断生成结构的 layout、歧义和编译成本

机制总览

第3章:Typelists:机制路径

  1. 1

    为什么需要 Typelists:The Need for …

    普通 container 保存“同一种元素类型的多个值”;generic component 常需要保存“多个不同类型本身”。Functor 要记录参数签名,Abstract Factory 要记录 product family,multimethod dispatcher 要记录参与类型。

  2. 2

    Defining Typelists

    Defining Typelists 使用二元节点: Head 保存当前类型, Tail 指向余下 Typelist; NullType 是终点。这个结构故意模仿 singly linked list,使模板 specialization 能写出递归算法。

  3. 3

    Linearizing Typelist Creation

    嵌套写法随着长度增长很难读。 Linearizing Typelist Creation 在书中通过 TYPELIST 1 到 TYPELIST N 宏把线性参数展开为嵌套节点。宏是当时缺少 variadic templates 的工程折衷:它不能检查类型数量以外的语义,且上限固定。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

第3章:Typelists:机制与证据

切换《第3章:Typelists》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么需要 Typelists:The Need for …

普通 container 保存“同一种元素类型的多个值”;generic component 常需要保存“多个不同类型本身”。Functor 要记录参数签名,Abstract Factory 要记录 product family,multimethod dispatcher 要记录参与类型。

可核验证据

用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要 Typelists:The Need for …」的组合规则与扩展边界。

学完《第3章:Typelists》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

第3章:Typelists:失效与核验

为什么需要 Typelists:The Need for …

典型失效

若只复制「为什么需要 Typelists:The Need for …」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。

核验证据

用正向与应拒绝的编译案例、生成类型和生命周期测试核对「为什么需要 Typelists:The Need for …」的组合规则与扩展边界。

Defining Typelists

典型失效

若只复制「Defining Typelists」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。

核验证据

用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Defining Typelists」的组合规则与扩展边界。

Linearizing Typelist Creation

典型失效

若只复制「Linearizing Typelist Creation」模板结构而不声明替换点、所有权和实例化边界,组合后的类型会迅速产生二义性或不可诊断错误。

核验证据

用正向与应拒绝的编译案例、生成类型和生命周期测试核对「Linearizing Typelist Creation」的组合规则与扩展边界。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么需要 Typelists:The Need for Typelists

普通 container 保存“同一种元素类型的多个值”;generic component 常需要保存“多个不同类型本身”。Functor 要记录参数签名,Abstract Factory 要记录 product family,multimethod dispatcher 要记录参与类型。The Need for Typelists 就是把这组类型变成编译器可遍历、查询和改写的数据结构。

Typelist 不创建对象,不保存 bytes,也不能在运行时 push_back。它的输出通常是另一个类型、一个 integral constant,或由类型序列生成的一套 class hierarchy。今天常用 parameter pack 表示同一概念,但“把类型当数据”的模型没有改变。

Defining Typelists

Defining Typelists 使用二元节点:Head 保存当前类型,Tail 指向余下 Typelist;NullType 是终点。这个结构故意模仿 singly linked list,使模板 specialization 能写出递归算法。

struct NullType {};
 
template<class H, class T>
struct Typelist {
    using Head = H;
    using Tail = T;
};
 
using UiTypes = Typelist<Button,
                Typelist<Window,
                Typelist<Dialog, NullType>>>;

NullType 必须与真实 element type 区分;若业务列表本身可能包含 NullType,termination 语义就含混。现代 parameter pack 通过空包 specialization 终止,不再需要 sentinel,但仍必须明确 empty sequence 的结果。

Linearizing Typelist Creation

嵌套写法随着长度增长很难读。Linearizing Typelist Creation 在书中通过 TYPELIST_1TYPELIST_N 宏把线性参数展开为嵌套节点。宏是当时缺少 variadic templates 的工程折衷:它不能检查类型数量以外的语义,且上限固定。

现代写法可直接定义 template<class... Ts> struct TypeList {};,或用 alias 将 parameter pack 转为旧 Head/Tail 结构。迁移时不要只替换语法:若后续算法依赖 Head/Tail,需要一起改为 pack specialization 或提供兼容 adapter。

Calculating Length 与 Indexed Access

Calculating Length 是最小递归例子:普通节点的长度为 1 + Length<Tail>NullType 特化返回 0。值在编译期产生,不需要任何 object。

template<class List> struct Length;
 
template<class H, class T>
struct Length<Typelist<H, T>> {
    static constexpr std::size_t value = 1 + Length<T>::value;
};
 
template<>
struct Length<NullType> {
    static constexpr std::size_t value = 0;
};

Indexed AccessTypeAt<List, i>i == 0 返回 Head,否则转成 TypeAt<Tail, i - 1>。越界时没有匹配特化,形成编译错误;TypeAtNonStrict 则可在越界时返回 caller-supplied default。两种 contract 都合理,但必须由 API 名和诊断明确。

Searching Typelists

Searching TypelistsIndexOf<List,T> 比较当前 Head:相同返回 0;否则递归查询 Tail,找到则加 1,未找到返回 -1。这里的难点是不能对 -1 继续加 1,否则“未找到”会变成合法位置。

IndexOf 的 equality 是 exact type equality;const WidgetWidget&Widget 不同。若业务想按 normalized type 搜索,应在入口明确 remove_cvref,而不是悄悄改变所有 typelist 算法的语义。

Appending to Typelists

Appending to Typelists 不修改原列表,而是产生新类型。Append<NullType, X> 形成一个节点;Append<Typelist<H,T>, X> 保留 H,并递归 append 到 T。若 X 本身是 Typelist,可以 splice 整条序列,contract 需区分“添加一个 element type”和“连接两个 lists”。

Erasing a Type from a Typelist

Erasing a Type from a Typelist 在第一个 matching Head 处返回 Tail,其余层重建原 Head。EraseAll 则匹配时继续递归,不匹配时保留节点。两个函数只差一处递归选择,却表达完全不同的 duplicate contract。

template<class List, class T> struct Erase;
template<class T> struct Erase<NullType, T> { using Result = NullType; };
 
template<class T, class Tail>
struct Erase<Typelist<T, Tail>, T> { using Result = Tail; };
 
template<class Head, class Tail, class T>
struct Erase<Typelist<Head, Tail>, T> {
    using Result = Typelist<Head, typename Erase<Tail, T>::Result>;
};

Erasing Duplicates 与 Replacing an Element

Erasing DuplicatesNoDuplicates)先去重 Tail,再从结果中删掉当前 Head 的后续副本,最后把 Head 接回;因此保留第一次出现顺序。若先删 Head 再递归,可能改变 survivor choice。元算法的顺序就是语义,不只是实现细节。

Replacing an Element in a Typelist 与 Erase 类似:遇到第一个 old type 时用 new type 构造节点并停止,ReplaceAll 则继续。替换后可能产生 duplicate,是否再运行 NoDuplicates 由 caller contract 决定,不能在 Replace 内隐式做。

Partially Ordering Typelists

当列表同时含 BaseDerived,dispatcher 通常要先尝试更具体的 Derived。Partially Ordering Typelists 根据 inheritance relation 交换次序,使 derived classes 出现在 bases 之前;互不相关类型保持某种稳定顺序。

这不是全序:两个 sibling 没有自然先后。算法若依赖 input order 保持稳定,应写测试;ambiguous/private inheritance 与 conversion operator 也不能简单等同 public base relation。现代实现使用 std::is_base_of 加明确 accessibility contract。

Class Generation with Typelists

Class Generation with Typelists 把列表的每个类型映射成一个 Unit<T>,再按结构组合。scatter hierarchy 让生成类同时继承 Unit<T1>、Unit<T2>...;每种 T 对应一个 sibling base,适合 heterogeneous tuple 和“每类型一个字段”。

当列表重复同一类型,两个 Unit<T> base 会歧义。Loki 通过 indexed wrapper 或不同中间类型区分位置;访问 tuple field 也要决定按 type 还是按 index。按 type 访问在 duplicate 存在时天然不唯一。

tuple class 在书中由 scatter hierarchy 生成 storage,再提供 Field<T>/Field<i> 访问。它展示“类型列表是 schema,hierarchy 是 materialized representation”:列表顺序、duplicate 与 Unit layout 都影响最终 object。

linear hierarchy 则生成 Unit<T1, Unit<T2, Unit<T3, Root>>> 的链,每个 Unit 只连接下一个,适合 ordered handlers、chain of responsibility 或逐层 policy decoration。

先预测:Typelist<Base, Derived> 直接生成 dispatcher 时,若先测试 Base,Derived object 也会命中 Base 分支,具体处理器永远不可达。先做 inheritance partial order 不只是优化,而是 correctness requirement。

分步1 / 3

第一步:写出表示与终止点

手工展开三层 Head/Tail 链,标记 NullType;再为 empty、one-element、duplicate 与 out-of-range 定义 contract。

小结

  • Typelist 满足对有序异构类型集合的编译期表示需求,Head/Tail/NullType 给出递归数据结构
  • 线性化宏只改善书写,现代 parameter pack 改善表达;两者都需要明确 empty 与 bounds contract
  • Length、TypeAt、IndexOf 将链表算法搬到模板实例化,推进分支和终止特化同等重要
  • Append、Erase、NoDuplicates、Replace 产生新类型,执行顺序决定 duplicate 与 survivor 语义
  • inheritance partial order 把 Derived 放在 Base 前,是类型 dispatcher 的 correctness 基础
  • scatter hierarchy 生成并列 Unit,适合 tuple;linear hierarchy 生成有序链,适合 handler pipeline
  • 运行时零分派不代表免费:编译时间、code size、layout、歧义和 ABI 都是设计成本

练习

  1. 问题 1:手工推导元算法。Typelist<int, Typelist<double, Typelist<int, NullType>>> 计算 Length、TypeAt<1>、IndexOf<int>、NoDuplicates 的结果。
  1. 问题 2:选择 scatter 或 linear hierarchy。 配置对象要为每种 type 保存一个值;middleware 要按顺序逐层处理 request,分别选择结构并说明原因。
  1. 问题 3:修复继承分派顺序。 输入列表为 Base、Sibling、Derived,dispatcher 按顺序 dynamic_cast;说明应建立哪些偏序约束。

名词解释

名词解释

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

Typelist
在编译期保存有序类型序列并供模板元算法处理的类型结构。
Head
当前 Typelist 节点承载的元素类型。
Tail
当前节点之后的剩余类型链。
Length
沿 Tail 递归计算类型序列长度的元函数。
TypeAt
按零基索引返回对应元素类型的元函数。
IndexOf
返回目标类型第一次出现位置的编译期搜索。
Append
在类型序列末端追加元素或另一列表的变换。
NoDuplicates
移除重复类型并保留首次出现顺序的变换。
Replace
替换一个或全部目标类型的序列变换。
inheritance partial order
以派生类先于基类为约束的类型偏序。
scatter hierarchy
把每个类型映射成并列 Unit base 的生成结构。
linear hierarchy
按列表顺序把 Unit 嵌套为单链继承结构。

资料与写作方式声明

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

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

讨论

评论区加载中…