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

类型萃取的设计与实现

概述

  1. 类型萃取(type traits)可以理解成编译期函数

  1. 它们主要通过:
    1. 类模板
    2. 偏特化
    3. using 类型成员
    4. static constexpr 成员

类型萃取的三种主要形式

类型 输入输出 例子
预测型 trait 类型 → bool is_same、is_pointer
转换型 trait 类型 → 类型 remove_reference、decay
属性型 trait 类型 → 值或元数据 sizeof、iterator_traits
  1. 典型使用形式

std::integral_constant

  1. std::integral_constant 是很多标准类型萃取的基础

  1. 它把一个编译期数值同时包装成
    1. 一个类型
    2. 一个静态常量
    3. 一个可以构造的空对象

  1. 之所以需要把一个值变成类型,是因为类型可以:
    1. 参与函数重载
    2. 作为模板参数
    3. 作为基类
    4. 被偏特化匹配

true_typefalse_typebool_constant

  1. 标准库定义:

  1. C++17 又提供

  1. 于是可以写出两个重载

实现 is_same

  1. 目标

  1. 实现

  1. 一般情况
    1. 默认认为两个类型不同

  1. 偏特化
    1. 只有两个参数是完全相同的类型时才匹配

  1. 使用

为什么要继承 true_typefalse_type

  1. 也可以写成

  1. 但继承 std::true_type/std::false_type 可以自动获得

  1. 而且 trait 本身可以作为 tag 使用

  1. 所以预测型 trait 通常写成

实现 remove_reference

  1. 目标

  1. 实现

  1. 使用

  1. 注意:
    1. remove_reference 只删除引用,不删除 const

为什么需要三个版本

  1. 主模板:
    1. 处理普通类型

  1. 偏特化
    1. 专门匹配左值引用

  1. 另一个偏特化
    1. 专门处理右值引用

remove_cvremove_cvrefdecay

  1. 容易混淆的四个 trait
输入 T remove_reference_t<T> remove_cvref_t<T> decay_t<T>
const int& const int int int
int&& int int int
const int[3]& const int[3] int[3] const int*
函数引用 函数类型 函数类型 函数指针
  1. C++20

  1. 下面这样通常没有效果
    1. 因为引用类型自身没有顶层 constconst 修饰的是它引用的 int

  1. 所以
    1. 只想获得对象基础类型:常用 remove_cvref_t
    2. 想模拟按值传参:使用 decay_t

实现 conditional

  1. std::conditional 是编译期的类型选择

  1. 简化实现

  1. 使用
    1. 主模板处理 truefalse 偏特化覆盖另一种情况

conditional 不是普通运行期 if

  1. 64 位平台上
    1. 最终程序中没有运行期分支

  1. 要注意:传给 conditional 的三个模板参数必须首先能够被写出来

  1. 要延迟求值,需要传递“trait 包装器”,最后才访问 ::type

  1. C++20 标准库提供了对应的

trait可以描述外部类型

  1. trait 的重要价值是:
    1. 不需要修改目标类型,也能给它附加编译期信息
  2. 例如协议消息

  1. trait 不能保证
    1. 消息一定正确序列化
    2. max_size 与真实编码结果一致
    3. 网络字节序已经转换
    4. 异步发送期间对象仍然存活

累加 trait

traittag 的关系

  1. 常见流程是

  1. 例如
    1. iterator_traitstrait
    2. iterator_category 是提取出的 tag 类型
    3. category{}tag 对象
    4. advance_impl() 通过重载选择不同算法

Tag Dispatch

概述

  1. 核心只有一句话
    1. 把编译期信息包装成不同的类型,然后利用函数重载选择不同实现
  2. 它通常由三部分组成
    1. 它不是运行期分支,通常不会生成类似 if 的运行期判断

最小完整示例:布尔 Tag

  1. 调用

Tag 类型和 Tag 对象

  1. 定义
    1. 这是一个 tag 类型

  1. 构造
    1. 这是一个临时 tag 对象

  1. 也可以提供一个命名对象

  1. 于是调用者可以写
    1. binary_mode_t 是类型
    2. binary_mode 是对象
    3. 函数参数通过 binary_mode_t 区分重载

  1. 标准库中有很多类似设计

Tag 为什么通常是空类

  1. Tag 为什么通常是空类
    1. 分类
    2. 能力
    3. 模式
    4. 编译期选择

  1. 虽然空类对象通常仍满足
    1. 但临时 tag 对象通常可以被编译器优化掉
    2. Tag dispatch 的关键优势来自编译期重载选择,并不是“空类的大小一定为零”

  1. 如果参数需要携带运行期数据,例如
    1. 它更像配置或策略对象,而不是纯 tag

为什么不直接传 bool

  1. 下面不是 tag dispatch

  1. 真正的 tag dispatch 使用不同类型

  1. 如果条件本身是编译期常量,可以转换为布尔 tag

最经典的使用:迭代器 Tag

  1. 不同迭代器具有不同能力
Tag 主要能力
input_iterator_tag 向前读取一次
forward_iterator_tag 可重复向前遍历
bidirectional_iterator_tag 可以 ++--
random_access_iterator_tag 支持 +=、下标、距离
contiguous_iterator_tag C++20,元素还保证连续存储
  1. 它们具有继承关系
    1. output_iterator_tag 是另一条独立分类,不属于这条继承链

  1. 这种继承表达的是
    1. 随机访问迭代器也具备双向、前向和输入迭代能力

实现 advanceTag Dispatch

std::list 如何完成派发

  1. 对于 std::list<int>::iterator

std::vector 为什么选择随机访问版本

  1. 对于 std::vector<int>::iterator

为什么需要一个公共包装函数

  1. 可以让用户直接调用
    1. 但这会暴露实现细节,而且用户可能传错 tag

  1. 因此标准结构是

  1. 公共包装函数负责
    1. 推导模板参数
    2. 通过 trait 提取正确 tag
    3. 调用内部实现
    4. 防止调用者手动传错 tag

Trait 不会自动验证 Tag 是否真实

  1. 例如

  1. 因此 tag 是一种类型级承诺
    1. 我声明自己属于这个类别,并满足这个类别对应的接口和语义
  2. C++20 Concepts 能更直接地检查部分操作是否存在
    1. 但一些语义要求,例如“多次遍历得到相同结果”,仍然无法完全由编译器证明

布尔Tag与分类Tag

  1. 布尔 Tag只有两个分支
    1. 适合“是或否”的能力

  1. 分类 Tag存在多个类别
    1. 适合描述分层能力

  1. 模式 Tag
    1. 由调用者主动选择
    2. 适合区分构造方式或 API 语义

网络缓冲区中的 Tag Dispatch

  1. 假设网络发送支持两类缓冲区
    1. 连续缓冲区:一次普通发送
    2. 分段缓冲区:使用 writevWSASendscatter/gather
  2. 定义tag

  1. 定义 trait

  1. 假设有分段缓冲类型

  1. 内部实现

  1. 公共接口

  1. 不需要知道最终是普通 send()writev() 还是 WSASend()
  2. tag dispatch 不会处理(它只负责选择实现)
    1. 部分发送
    2. 断线
    3. 缓冲区生命周期
    4. 并发关闭 socket
    5. 异步操作取消

继承型Tag的隐藏优势:自动回退

  1. 假设有

  1. 只实现通用版本

进阶:priority_tag

  1. 大型模板项目中还会看到

  1. 用来表达多级回退

  1. 选择顺序
    1. 如果有 value.dump(),选择优先级 2
    2. 否则,如果支持 operator<<,选择优先级 1
    3. 否则,退回优先级 0
  2. 这里结合了:
    1. Tag 继承
    2. 重载解析
    3. SFINAE
    4. 表达式检测

Tag Dispatchif constexprConcepts怎么选

技术 主要目的 适用场景
Tag dispatch 根据类型分类选择实现 有稳定类别或继承层次
if constexpr 在一个模板内部选择分支 分支较少、逻辑紧密
SFINAE 排除无效候选 C++11/14/17 接口约束
Concepts 声明接口要求并参与重载排序 C++20 泛型接口
普通 if 根据运行期值选择 条件运行时才知道
  1. C++17 可以把简单的布尔 tag 改写成
    1. 这种写法更短

  1. tag dispatch 仍适合
    1. 不同实现很长,需要分开
    2. 多个算法共享同一分类
    3. 分类具有继承层次
    4. 需要多级自动回退
    5. 维护 C++11/14 项目
  2. C++20 Concepts 更适合控制“这个接口能不能调用”
    1. tag dispatch 更偏向:类型已经合法,现在选择哪种实现

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

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

发表评论

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