• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-08-11 15:00 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

模板定制体系

概述

  1. 大型模板库必须解决一个核心问题
    1. 库作者写好了通用算法,但怎样允许用户为自己的类型提供特殊行为
  2. C++ 中没有唯一的定制机制,而是形成了一组工具
机制 最适合表达
traits 这个类型具有什么静态信息
policy 这个组件采用哪一种行为策略
tag dispatch 这次调用明确选择哪种实现
CRTP 派生类向模板基类提供实现
ADL 在类型所属命名空间中寻找操作
CPO 对外提供统一、受控的定制入口
Concepts 定制实现必须满足哪些要求

先建立总体模型

  1. 假设库提供

  1. 库可以按照以下优先级寻找实现
    1. 这个统一入口就是 CPO
    2. 背后的成员函数和 ADL 是定制通道
    3. Concepts 控制通道是否合法

Traits:把类型映射到静态信息

  1. 假设不同网络消息对应不同命令号

  1. 可以定义 traits

  1. 为每种消息提供特化

  1. 通用代码

  1. Traits 的核心模型是

  1. 例如标准库

  1. Traits 可以包含什么
    1. Traits 不一定只是 std::true_typestd::false_type 一类布尔判断

  1. Traits 增加 Concept

  1. Traits 的适用范围

  1. 还有一个重要限制
    1. traits 的显式特化通常必须定义在主模板所属的命名空间中

Policy:把行为作为模板参数

  1. Traits 通常回答“类型是什么”,Policy 通常回答“组件应该怎么做”
  2. 例如协议解析失败时,可以有不同策略

  1. 组件接收策略类型

  1. Policy 的模型是
    1. 组件算法保持不变
    2. 行为细节由模板参数注入
  2. 常见策略:
    1. 错误处理策略;
    2. 锁策略;
    3. 内存分配策略;
    4. 重试策略;
    5. 比较策略;
    6. 缓存淘汰策略;
    7. 日志策略;
    8. 序列化策略
  3. 为什么保存 Policy 对象

  1. Policy 的代价

Tag dispatch:用空类型选择实现

  1. Tag 是一个通常没有数据、只表达类别或意图的类型

  1. 通过重载选择实现

  1. Tag 比布尔参数更清楚

  1. 类别 tag dispatch

CRTP:派生类作为模板参数

  1. Curiously Recurring Template Pattern,奇异递归模板模式
  2. 基本形式

  1. CRTP 与虚函数
CRTP 虚函数
派生类型编译期已知 具体类型运行时确定
通常容易内联 通过虚表动态分发
不需要虚表 对象通常含虚表指针
类型之间强耦合 调用方可只依赖基类
不能自然存放异质派生对象 可通过基类指针统一存放
改变模板实现可能引发重新编译 更适合稳定动态接口
  1. CRTP 的风险
    1. CRTP 依赖一个约定:Derived 必须是真正的派生类型

ADL:让操作跟着类型走

  1. Argument-Dependent Lookup,实参依赖查找
    1. 它会根据函数实参的类型,到关联命名空间中查找候选函数

  1. swap 的经典写法

  1. Hidden friend
    1. 可以把定制函数定义成类内友元

  1. ADL 的陷阱
    1. 过于宽泛的函数模板可能意外加入候选集合
    2. 引入第三方类型后,关联命名空间发生变化
    3. 同名函数容易产生重载歧义
    4. 限定调用会关闭 ADL
    5. 不限定调用又可能发生递归
    6. 很难从调用点看出最终函数来自哪里

CPO:统一的定制点对象

  1. Customization Point Object,定制点对象
    1. 它通常是一个 inline constexpr 函数对象

  1. CPO 可以在内部明确规定:
    1. 优先调用成员函数;
    2. 否则尝试 ADL
    3. 否则采用默认实现
    4. 都不满足则让约束失败

一个完整的 CPO 示例

  1. 下面的 CPO 支持两种定制方式:
    1. message.encode(encoder)
    2. ADL 找到 encode_value(encoder, message)
  2. 成员函数优先

  1. 为什么 ADL 函数不叫 encode

CPO 比裸函数好在哪里

  1. 普通函数模板

  1. CPO 对象
    1. 自身不能像普通函数那样被随意添加重载
    2. 定制顺序由 operator() 内部控制
    3. 可以使用 Concepts 明确候选条件
    4. 可以作为对象传递给高阶算法
    5. 可以统一成员、ADL和默认行为
    6. 公共调用形式稳定

tag_invoke 是什么

  1. 一些库使用统一的底层 ADL 函数

  1. 概念形式

怎样选择定制机制

需求 推荐机制
类型映射到命令号或关联类型 traits
组件选择错误、锁或分配策略 policy
调用方明确选择阻塞/非阻塞 tag 参数
基类调用派生类静态实现 CRTP
操作自然属于用户类型命名空间 ADL/hidden friend
公共库需要统一稳定入口 CPO
检查定制实现是否合法 Concepts
类型集合运行时变化 虚函数或类型擦除

网络组件中的推荐分层

常见错误

  1. 随意特化 std 中的模板
    1. 只有标准明确允许时才能添加特化。通常应定义自己的 traits
  2. 把运行时状态放进静态 traits
    1. 个连接的重试次数不同,就应该存放在策略对象或连接对象中,而不是

  1. Policy 组合过多
    1. 会造成复杂类型、代码膨胀和编译依赖。并非所有变化维度都应该模板化

  1. CRTP 当运行时多态
    1. CRTP 本身不能提供统一的非模板运行时基类接口

  1. 限定调用意外关闭 ADL

  1. CPOADL 使用相同名字导致递归
    1. 公共对象与内部 ADL 协议最好明确隔离
  2. Concept 检查通过就认为运行时安全
    1. Concept 无法证明引用生命周期、线程安全和协议数据合法性

模板实例化与工程构建

从模板到具体函数

模板不会一次性实例化所有内容

  1. 考虑类模板

  1. 因此
    1. 类模板被使用,不等于所有成员函数体都立即实例化

为什么模板通常放在头文件

  1. 普通函数可以这样分离

  1. 调用者只需要知道函数签名,因为 add.cpp 会直接生成唯一的 add(int,int)
  2. 模板不同

  1. 所以最常见写法是把定义放在头文件:

.tpp 文件不是真正的单独编译

  1. 有些项目这样组织:

  1. 然后在头文件末尾:
    1. 这只是把模板实现从视觉上分离
    2. 预处理之后,.tpp 内容仍然出现在每一个包含 vector.hpp 的翻译单元中,并不是像普通 .cpp 一样独立编译

多个 .cpp 都实例化同一个模板怎么办

  1. 假设:

  1. 典型编译器会把这类实体放入 COMDATweak symbol 或类似链接器机制中,最终保留一个等价定义
  2. 具体机制不由 C++ 标准规定,但 ODR 允许满足条件的模板定义出现在多个翻译单元中
  3. 因此
    1. 头文件中定义模板 必然产生 multiple definition

ODR:单一定义规则

  1. ODROne Definition Rule
    1. 某些实体在整个程序中只能有一个定义
    2. 模板、类定义和 inline 实体可以在多个翻译单元出现
    3. 这些多份定义必须满足严格的一致性要求
  2. 模板最危险的 ODR 问题之一是宏导致定义不同

隐式实例化

  1. 正常调用触发的就是隐式实例化

显式实例化定义

  1. 可以明确要求编译器生成某个特化

  1. 它的含义不是“为 Login 编写特殊算法”,而是
    1. 使用通用模板定义,明确生成 Parser<Login> 的相关实体
  2. 类模板的显式实例化定义还会涉及当时定义可见、符合规则且尚未特化的非模板成员。若只需要某个成员,也可以单独实例化

显式实例化声明:extern template

  1. 头文件

  1. 实现文件

  1. 其他翻译单元

  1. extern template 表示
    1. 当前翻译单元不要为这个模板实参组合进行通常的隐式实例化
    2. 程序中的其他位置会提供显式实例化定义

  1. 果只有 extern template,却忘记提供显式实例化定义,通常会出现链接错误

extern template 不保证解决所有编译时间问题

  1. extern template 可能减少
    1. 多个翻译单元重复生成机器代码
    2. 优化器对同一特化进行重复优化
    3. 对象文件中的重复模板实体
  2. 但它不一定消除:
    1. 头文件预处理
    2. 模板定义解析
    3. 名字查找
    4. 约束检查
    5. 为类型检查而进行的部分实例化
    6. 内联相关分析
    7. 调用点的模板实参推导

显式实例化与显式特化的区别

  1. 显式实例化
    1. 算法没有改变

  1. 显式特化

函数模板显式特化的头文件问题

  1. 函数模板

  1. 如果在头文件定义显式特化
    1. 这个显式特化本身不再是普通意义上的函数模板
    2. 多个翻译单元包含它,可能出现多重定义

  1. 如果必须定义在头文件,应考虑

模板静态变量

  1. C++17inline static 允许在类定义中直接定义

  1. 每个模板特化拥有自己的静态变量
    1. 它们是两个不同实体

  1. 同样,函数模板的局部静态变量也是每个特化一份

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享