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

Typelist——编译期类型容器

概述

  1. 普通容器保存运行期对象

  1. Typelist 保存的是一组类型
    1. 它没有运行期元素,也不负责构造对象
    2. 所有内容都编码在模板参数包中

  1. C++ 标准库没有名为 std::typelist 的类型,但:
    1. std::tuple<Ts...> 保存一组不同类型的对象
    2. std::variant<Ts...> 保存其中一种类型的对象
    3. std::integer_sequence<T, Values...> 保存一组编译期数值
    4. 大型项目经常定义自己的 TypeList
    5. Boost.MP11 等库提供了成熟的类型列表算法

最小TypeList

  1. 使用

  1. Typelist 本身通常是空类
    1. 这个对象不包含 intlongfloatdouble 对象

Typelist是不可变的

  1. 向列表添加类型不会修改原列表,而是生成一个新类型

实现Size

  1. 也可以提供变量模板

实现Front

  1. Front 返回第一个类型

  1. 对于空列表
    1. 没有对应偏特化,因此不合法
    2. 这表示 Front 的前置条件是列表非空

实现PopFront

  1. PopFront 删除第一个类型,并生成新列表

  1. 这里不是删除运行期对象,而是重新构造一个模板特化

实现PushFront

实现PushBack

  1. 对于参数包形式的 TypeList,可以直接展开

基础操作完整程序

实现 At:按索引取类型

  1. 如果索引越界,递归最终作用于空列表,编译失败
  2. 标准库对于元组有相似操作

Transform:转换每个类型

  1. 目标

  1. 实现

  1. 核心展开

  1. 我们的 Transform 要求元函数遵循 trait 接口

Transform可能提前失败

  1. 下面会失败

  1. Transform 会对列表中的每个元素实例化元函数
    1. 它不会自动跳过不适用的类型
  2. 正确流程通常是

Filter:过滤类型

  1. 目标

  1. 实现

Filter如何保持顺序

  1. 输入

  1. 所以原有相对顺序得到保留

自定义Predicate

  1. Predicate 是输入类型、输出布尔值的元函数

  1. 注意 sizeof(T) 不等于复制成本,也不能完整描述:
    1. 构造是否昂贵
    2. 是否动态分配内存
    3. 是否可平凡复制
    4. 是否适合按值传递

组合TransformFilter

  1. 假设输入

  1. 先删除 cv/ref

  1. 再过滤整数

TypeList转成tuplevariant

  1. Typelist 只描述类型
    1. 最终通常需要把它应用到实际类模板上
  2. 实现 Rename

  1. 这种操作在不同库中可能叫
    1. Rename
    2. Rebind
    3. Apply
    4. Instantiate
    5. Wrap
  2. 可以理解为
    1. 把类型列表中的参数包重新传给另一个类模板

TypeListTupleVariant的区别

类型 是否保存对象 运行期含义
TypeList<Ts...> 纯编译期类型集合
std::tuple<Ts...> 同时保存每种类型的一个对象
std::variant<Ts...> 任意时刻保存其中一种类型
std::integer_sequence<T, Vs...> 编译期数值序列
  1. 例如

  1. 另外,std::variant 的备选类型不能是:
    1. void
    2. 引用类型
    3. 数组类型
    4. 如果 TypeList 经过转换后要生成 variant,必须保证结果满足这些要求

把类型列表展开为代码

  1. 可以将每个类型包装成 tag,再执行参数包展开

  1. 这里没有创建 T 对象,只构造了

  1. C++20 也可以使用

  1. 这种技术可以用于
    1. 为一组消息类型生成处理器
    2. 为一组测试类型生成测试用例
    3. 为所有协议类型生成序列化函数表
    4. 对一组组件类型执行静态注册

网络消息中的实际应用

  1. 定义 trait

  1. 定义 Predicate

  1. 过滤

  1. 生成 variant

编译成本与现代化建议

  1. 对于具体的参数包 TypeList
    1. Size:直接 sizeof...
    2. `Front:一次偏特化
    3. PopFront:一次偏特化
    4. PushFrontPushBack:一次参数包展开
    5. Transform:每个类型实例化一次元函数
    6. Filter:线性递归,约为 O(N) 层实例化
    7. At<N>:线性递归,约为 O(N)
  2. 大型列表的递归算法可能导致
    1. 编译时间增加
    2. 模板实例化深度过大
    3. 错误信息冗长
    4. 编译器内存消耗增加

std::tuple与异质存储

概述

  1. std::tuple<Ts...> 在一个对象中,分别保存参数包 Ts... 中的每一种对象
  2. 它与之前的类型列表有本质区别

std::tuple的基本使用

  1. message 中三个元素的类型完全不同

  1. 这里的 0 是模板实参,必须在编译期确定

从零实现一个递归TinyTuple

实现get<I>

  1. 可以利用 if constexpr 递归查找第 I 个元素

  1. 因为 if constexpr 不会实例化未选择的分支,所以递归能在 I == 0 时停止

为什么返回 decltype(auto)

  1. 假设错误地写成

  1. 即使内部返回的是引用
    1. 普通 auto 也会像变量类型推导一样丢掉引用,结果可能成为值类型

  1. decltype(auto)
    1. 使用 decltype 规则推导,可以保留返回表达式的引用性质
  2. 标准库 std::get 大致要保留这些类型
tuple表达式 普通元素对应的返回类型
tuple& T&
const tuple& const T&
tuple&& T&&
const tuple&& const T&&

按类型访问:std::get<T>

  1. 除了索引,标准库还支持

  1. 但前提是目标类型在 tuple 中恰好出现一次

  1. 如果一个返回值具有稳定的业务含义,工程中经常应该考虑使用结构体:
    1. tuple 更适合局部组合、泛型工具和临时返回值,不适合代替所有业务结构体

简单递归实现的空间问题

  1. 递归终点
    1. 是一个空类型

  1. 但一个独立的空对象通常不能完全没有大小
    1. 这是因为两个不同对象通常需要能拥有不同地址

  1. 因此
    1. empty 可能占用额外空间或引起对齐填充

  1. TinyTuple 最深处也包含一个空的 TinyTuple<> 成员,因而可能造成空间浪费

EBO:空基类优化

  1. 如果空类型作为基类存在,编译器通常可以不给它分配额外空间
    1. Empty Base Optimization,空基类优化,简称 EBOEBCO

  1. 但不要写这样的可移植断言
    1. EBO 是否发生以及具体布局仍然与编译器、ABI和类型关系有关

让递归 tuple 继承 tail

  1. 可以改为

  1. 递归终点 CompressedTuple<> 现在成为空基类,而不是空成员
    1. 编译器可以对它应用 EBO
  2. 但是这仍然只优化了空的递归尾节点,没有完整解决空元素的问题
    1. 如果 Empty 仍以普通成员保存,它仍然可能占空间

标准库风格:带索引的存储叶子

  1. 一种更接近工业实现的模型是

  1. 然后让 tuple 同时继承所有叶子

  1. 考虑

  1. 所以大型模板实现中的索引经常不仅用于查找,还用于:
    1. 给相同类型的不同位置创建唯一身份

对空元素应用 EBO

  1. C++20 之前,常见做法是根据类型是否为空,选择成员存储或继承存储

  1. 为什么要检查 !std::is_final_v<T>
    1. 因为 final 类型不能被继承
    2. 因此,即使它是空类型,也只能采用成员存储
  2. 真实标准库实现还要处理
    1. 构造函数和约束
    2. 引用类型
    3. const 和右值
    4. 异常规范
    5. allocator-aware construction
    6. 不同类型之间的转换构造
    7. 比较运算
    8. ABI兼容性

C++20:[[no_unique_address]]

  1. C++20 提供了更直接的成员级空间优化机制

  1. 它告诉编译器
    1. 如果对象模型允许,该成员不必拥有独占的存储地址

  1. 这项特性在以下场景很常见
    1. allocator
    2. deleter
    3. policy object
    4. comparator
    5. hash functor
    6. stateless callback
    7. tuple-like storage
  2. 但需要注意
    1. 它是允许优化,不是保证特定大小
    2. 对齐和 ABI 仍可能影响结果
    3. 重复的相同空类型不一定都能重叠
    4. 不应该根据这种对象布局设计网络协议或磁盘格式

递归 tuple 与索引叶子 tuple

特性 递归 索引叶子继承
理解难度 较高
get<I> 递归进入 tail 定位唯一叶子
重复类型 可以保存 通过索引获得唯一身份
空类型优化 需要额外处理 容易按叶子应用 EBO
模板实例化深度 随索引递归 通常更扁平
工业实现价值 教学为主 更接近标准库实现
  1. 不同标准库的真实实现并不要求采用完全相同的内部结构。不能依赖:
    1. sizeof(std::tuple<...>)
    2. 或元素在内存中的排列顺序

make_tupletieforward_as_tuple

  1. std::make_tuple
    1. std::make_tuplestd::reference_wrapper<T> 有特殊处理,结果元素会成为 T&

  1. std::tie
    1. std::tie 创建左值引用的 tuple
    2. tie 不拥有对象。被引用对象必须继续存活

  1. std::forward_as_tuple
    1. 它创建由万能引用组成的 tuple
    2. 它的用途是暂时保存参数的值类别,以便稍后继续完美转发
    3. forward_as_tuple 适合在同一个完整表达式或严格受控的同步调用链中转发,不适合保存到异步任务、容器或成员变量中

三者的选择表

工具 保存内容 拥有对象吗 典型用途
make_tuple(args...) 衰变后的值 打包并保存参数
tie(args...) 左值引用 解包、引用已有变量
forward_as_tuple(args...) 万能引用 临时保留值类别并继续转发
tuple{args...} CTAD推导出的元素 通常是值 直接构造 tuple

结构化绑定

  1. C++17 可以使用结构化绑定

  1. 这里的 auto 会创建一个隐藏的 tuple 副本,idname 对应这个副本的元素

  1. 如果希望绑定原对象

  1. 常见形式

std::apply 的原理

  1. 假设有:

  1. 参数保存在 tuple

  1. 使用

简化版 apply

为什么使用 std::invoke

  1. 如果只写
    1. 普通函数、lambda 和函数对象通常可以工作
    2. 但成员函数指针需要特殊语法

  1. std::invoke 统一支持
    1. 普通函数
    2. lambda
    3. 函数对象
    4. 成员函数指针
    5. 数据成员指针
    6. std::reference_wrapper

std::apply 的值类别传播

  1. 观察

  1. 如果传给 apply 的 tuple 是左值,元素通常作为左值传递

  1. 如果 tuple 是右值
    1. 元素可以作为右值继续传递

网络项目中的例子

  1. 假设解析网络包头后得到两个字段

  1. 原因包括:
    1. tuple 的内存布局没有协议保证
    2. 元素顺序不等于物理内存顺序
    3. 可能存在对齐和填充
    4. 不同标准库实现可能不同
    5. 不同编译器、ABI和版本可能不同
    6. 整数还有网络字节序问题

异步代码中的危险 tuple

  1. 下面这种 tuple 本身不拥有底层对象

  1. 如果保存到异步队列,必须确认
    1. ocket 在任务执行前不会销毁
    2. span 指向的缓冲区仍然存活
    3. 缓冲区不会被并发修改
    4. socket 的关闭与任务取消有同步保证
  2. 问题不在 tuple 本身,而在于 tuple 很容易把多个非拥有引用打包起来,让生命周期关系变得不明显
  3. 工程上常用的判断方法是
    1. 这个 tuple 被保存后,其中每一个元素究竟拥有资源,还是仅仅借用资源

其他

阅读模板的翻译方法

  1. 下面模板通常意味着:
    1. 给参数包中的每个元素编号
    2. 支持重复类型
    3. 根据索引选择某个元素
    4. 可能进行 EBO[[no_unique_address]] 优化

  1. 下面模板通常意味着:
    1. tuple、数组或参数包按索引展开
    2. 生成 get<I>(...)...
    3. 并行处理两个或多个参数包
    4. 将运行形式转换成编译期展开形式

  1. 下面模板通常意味着:
    1. 代码准备处理 tuple-like 对象
    2. 后面很可能会构造 make_index_sequence
    3. 随后用 get<I> 展开元素

  1. 对下面,要检查:
    1. 元素是否应该被移动
    2. tuple 后续是否还会使用
    3. 元素中是否含有引用
    4. f 的参数是按值还是按引用接收

  1. 对下面,第一反应应该是检查临时对象和异步生命周期

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

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2026-08-10
Everything will be better.

发表评论

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