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

惰性实例化与元函数组合

概述

  1. 示例

  1. 很多人会认为“不选择的分支不会编译”,但实际经常仍然报错
  2. 关键规则是
    1. std::conditional 只负责选择已经成功形成的两个类型,不负责阻止模板实参本身被形成

什么是惰性实例化

  1. 考虑

  1. 仅仅把模板特化当成不完整类型使用,通常不需要实例化其定义

  1. 但访问成员必须实例化

  1. 其他需要完整类型的操作也会触发实例化,例如

conditional_t 为什么可能提前失败

  1. 错误示例

  1. 但对于 double,即使条件是 false,仍然必须先形成三个模板实参:

  1. 编译器必须先知道传给 std::conditional_t 的类型是什么,才能调用它进行选择

增加一层模板包装

  1. 不要提前计算

  1. 而是把计算封装到类模板里

  1. 对于保留原类型的分支

  1. 然后先选择包装器,再访问被选中包装器的 type

为什么增加包装后就能工作

  1. std::conditional_t 接收的是
    1. 这里只需要形成两个类模板特化的名字,通常不需要实例化它们的定义

  1. 关键结构是

C++20 std::type_identity

  1. C++20 标准库提供了

  1. 近似实现

type_identity 还能阻止模板参数推导

  1. 考虑

  1. 可以使用 type_identity_t 让第二个位置不参与推导

  1. 这种位置叫非推导上下文
  2. type_identity 因而有两个常见作用
    1. 包装类型,延迟计算
    2. 阻止某个位置参与模板参数推导

元函数转发

  1. 假设要组合两个转换 trait

  1. 可以通过继承转发成员

  1. 这叫元函数转发
    1. trait 继承另一个 trait,让另一个 traittypevalue 等成员成为自己的结果
  2. 预测型 trait 也一样

为什么别名模板不适合所有中间步骤

  1. 别名模板很好用

  1. 但它有两个重要限制:
  2. 一:别名模板不能特化
    1. 需要特化时,必须使用类模板,再在外面提供别名

  1. 二:别名通常会立即要求结果

  1. 所以大型模板库通常同时提供

  1. 经验规则
    1. 中间组合阶段保留 trait 包装器,最终需要实际类型时再使用 _t

SFINAE-friendly trait

  1. 下面这个 trait 看起来可以计算加法结果

  1. 更好的实现是

  1. 这就是 SFINAE-friendly
    1. 对不满足条件的合理输入,trait 本身尽量不要立即爆炸;它应产生 false,或者安全地不提供结果成员

检测SFINAE-friendly trait

  1. 可以实现一个通用的 HasType

  1. SFINAE-friendly不是说任何错误都会消失,而是
    1. 它允许外层模板在替换上下文中检测失败,并将候选排除

验证操作后才能继续计算属性

  1. 假设我们想判断移动构造是否为 noexcept

  1. 如果类型根本不能移动构造,那么:

  1. 安全版本分两步
    1. 先用 void_t 检查构造表达式是否合法
    2. 只有合法时,偏特化才会被选择
    3. 在偏特化中计算该表达式是否为 noexcept
    4. 不合法时回到主模板,结果为 false

  1. 标准库已经提供

惰性布尔组合

  1. C++17 提供

  1. 例如

  1. std::conjunction 类似编译期逻辑与,并具有短路实例化语义
    1. 找到第一个 value == false 的 trait 后,后续 trait 不需要继续求值
  2. std::disjunction 找到第一个 true 后停止

if constexpr 是否能替代惰性trait

  1. 函数内部通常可以
    1. 这里未选中的语句被丢弃,所以比元函数包装直观

  1. 但以下场景仍然需要 trait
    1. 计算函数返回类型
    2. 计算类模板成员类型
    3. 选择基类
    4. 选择成员存储类型
    5. 在函数声明中进行 SFINAE
    6. 将类型计算结果传给其他模板
    7. 兼容 C++11/14
  2. 因此
    1. 函数实现分支:优先考虑 if constexpr
    2. 类型层面的组合与接口声明:trait 和惰性实例化仍然重要
    3. C++20 接口约束:优先考虑 Concepts

线程池中的实际场景

  1. 线程池常见声明

  1. std::invoke_resultSFINAE-friendly trait
    1. 如果调用表达式合法,它提供 type
    2. 如果调用表达式不合法,它不提供 type
  2. C++17 可以结合

  1. C++20 更适合写
    1. 这里先约束调用合法,再计算结果类型

模板元编程基础——递归、编译期循环与 integer_sequence

概述

  1. 模板元编程(TMP)指的是
    1. 利用模板实例化,在编译期计算数值、产生类型或者生成重复代码
  2. 现代C++ 中应当区分三种需求
需求 优先技术
计算编译期数值 constexpr/consteval
变换或筛选类型 类模板、偏特化、递归
展开参数包、元组、固定索引 fold expressionindex_sequence

最经典的递归元函数:阶乘

  1. 如果没有终止特化,编译器会不断实例化,最终超过模板实例化深度限制

数值计算优先使用 constexpr

  1. 传统模板递归是 C++98 时代的主要编译期计算方式
  2. 现代 C++ 更适合写

  1. 循环形式从 C++14 开始可以方便地用于 constexpr 函数

  1. 如果要求一定在编译期计算,C++20 可以使用

类型递归无法被普通函数代替

  1. 目标:无论数组有多少维,都删除全部数组维度

  1. 注意:

  1. 标准库已经提供

编译期循环的本质

  1. 模板没有传统意义上的

  1. 传统模板元编程用递归表示循环

  1. 而现代 C++ 更多使用参数包展开

std::integer_sequence

  1. C++14 引入

  1. 简化定义类似

index_sequence

  1. 索引最常使用 std::size_t,所以标准库提供简写

  1. 标准库还可以自动生成

如何取出索引参数包

index_sequence_for

  1. 如果已经有一个类型参数包

  1. 可以使用

为什么元组需要索引序列

  1. 普通容器元素类型相同

  1. std::tuple 中每个元素类型可能不同

  1. 这里的索引必须是编译期常量,不能写普通运行期循环

实现 tuple_for_each

为什么实现函数分成两层

  1. 公共函数只知道元组类型和长度

  1. 实现函数负责将一个序列类型拆成参数包

  1. 这种“两层函数”是大型模板代码中的固定模式

值类别和生命周期

  1. 实现中使用

  1. 如果传入左值元组
    1. 元素作为左值传给函数

  1. 如果传入右值元组
    1. 元素可能以右值形式传给函数,可以被移动

  1. 因此 tuple_for_each 的回调如果保存元素引用,可能产生悬空引用

std::apply 与索引序列

  1. 如果目的不是“逐个调用”,而是把元组元素一次性作为函数参数,可以直接使用 C++17

  1. 因此:
    1. 一个函数接收所有元素:std::apply
    2. 同一个函数逐个处理元素:自己实现 tuple_for_each
    3. 已经拥有参数包:直接 fold expression
    4. 只有一个长度 N:先用 make_index_sequence<N>

混合元编程:生成运行期代码

  1. 模板元编程不仅计算编译期数值,也可以根据编译期索引生成运行期运算
  2. 例如固定长度数组的点积

  1. 对于 N == 3,近似生成

  1. 这里
    1. 索引和代码结构在编译期生成
    2. 数组元素可能是运行期数值
    3. 如果输入数组本身是 constexpr,结果也可以在编译期计算

不要为了“展开循环”盲目使用TMP

  1. 现代编译器通常可以
    1. 自动内联
    2. 自动展开循环
    3. 自动向量化
    4. 使用 SIMD 指令
  2. 显式展开可能反而导致:
    1. 模板实例化数量增加
    2. 编译时间变长
    3. 二进制体积膨胀
    4. 指令缓存压力增加
    5. 错误信息变长

编译期循环方案对比

场景 推荐方式
编译期纯数值计算 constexpr 循环
已有参数包 fold expression
元组逐元素处理 index_sequence + 包展开
一个函数接收整个元组 std::apply
递归类型变换 类模板递归和偏特化
C++11/14 参数包副作用展开 数组/初始化列表技巧
运行期长度 普通 for/Ranges

其他

阅读模板的翻译方法

  1. 现在执行这个类型函数

  1. 作为另一个模板的参数时:
    1. 先传递尚未求值的类型函数包装器

  1. 检查:
    1. A<T>B<T> 只是包装器,还是已经通过 _t/::type 提前求值?

  1. 看到主模板没有 type
    1. 默认情况下没有结果,只有满足检测条件的偏特化才提供结果

  1. 看到继承
    1. Trait 正在转发 OtherTrait<T>typevalue

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

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

发表评论

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