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

非类型模板参数 NTTP

概述

  1. 前面使用的模板参数都是“类型”

  1. 非类型模板参数传递的是编译期常量值

固定容量的栈

值也是类型身份的一部分

  1. 下面是两个完全不同的类型

非类型模板实参必须是编译期常量

  1. 合法

  1. 不合法

Capacity 不会作为普通成员存储

  1. Capacity 是编译期值,不需要额外增加std::size_t capacity_;

函数模板的非类型参数

非类型模板参数也能从函数实参推导

  1. 最经典的是推导数组长度

使用 auto 声明 NTTP

  1. C++17 开始,非类型模板参数的类型也可以用 auto 推导

固定为某个类型,还是使用 auto

  1. 指定明确类型

  1. 使用 auto

允许哪些非类型模板参数

  1. C++17 及以前的主要类型
    1. 整数类型
    2. 枚举
    3. 指针
    4. 左值引用
    5. 成员指针
    6. std::nullptr_t

  1. C++20 进一步支持
    1. 浮点数
    2. 满足要求的结构化字面量类类型
    3. 无捕获 Lambda 等结构化类型

C++20使用字符串作为模板值

  1. 不能直接写

  1. 但可以定义结构化的编译期字符串
    1. 它的成员:
    2. 是公开的
    3. 不是 mutable
    4. 类型本身可以用于编译期比较和身份确定

  1. 这种技术在现代项目中用于
    1. 编译期字段名
    2. 日志标签
    3. 命令名称
    4. 反射模拟
    5. 编译期路由
    6. SQL 字段映射

指针和函数地址作为 NTTP

  1. 函数地址可以作为模板参数

  1. 这是一种编译期回调绑定
    1. 适用于:
    2. 协议命令静态分发
    3. 中断处理器绑定
    4. 固定回调策略
    5. 无运行期修改需求的状态机

引用作为 NTTP

  1. 也可以使用具有合适存储期的对象引用

std::integral_constant

  1. 标准库把“类型”和“编译期值”绑定在一起

  1. 例如

什么时候使用 NTTP

  1. 适合:
    1. 容量影响对象布局
    2. 数组长度需要保留在类型中
    3. 值需要参与编译期计算
    4. 只存在少量固定配置
    5. 希望不同配置成为不同类型
    6. 编译期策略或静态回调绑定

  1. 不合适
    1. 值来自运行期
    2. 需要在运行期动态修改
    3. 可能出现大量不同值
    4. 不希望不同值产生不同类型
    5. 需要稳定、统一的 ABI 接口

变参模板与参数包

概述

  1. 变参模板从 C++11 引入,允许模板接收数量不固定、类型不同的参数

  1. 核心是
    1. 参数包代表编译期的一组参数,但不能把它当成普通变量使用;必须通过 ... 展开

模板参数包与函数参数包

  1. typename... Types
    1. 是模板参数包,保存一组类型
  2. Types... args
    1. 是函数参数包,保存一组函数实参

  1. 参数包只能在允许的语法位置展开或查询数量

递归展开参数包

  1. C++11 的经典实现

  1. 调用

  1. 为什么要先取出第一个参数

包展开是什么

  1. ... 不只是展开一个名字,它会展开包含参数包的整个模式

  1. 示例

sizeof...:获取参数个数

  1. sizeof...(Types)
    1. 模板类型参数个数
  2. sizeof...(args)
    1. 函数参数个数

  1. 它得到的是参数数量,不是内存字节数

为什么普通 if 不能停止实例化

  1. 可能想这样写
    1. Rest 为空时,运行期条件恒为 false,但普通 if 的函数体仍然必须通过编译
    2. 如果没有无参数重载,就会报错

  1. C++17:使用 if constexpr
    1. if constexpr 的未选择分支不会针对当前模板实例进行正常实例化,因此不需要无参数终止函数

C++17折叠表达式

  1. 递归模板能工作,但会产生很多递归实例
  2. C++17 引入折叠表达式,可以把某个二元运算符应用到整个参数包

折叠表达式的四种形式

  1. 假设参数包为

  1. “左折叠”和“右折叠”描述括号结合方向
形式 名称 展开结果
(... op pack) 一元左折叠 ((p1 op p2) op p3)
(pack op ...) 一元右折叠 (p1 op (p2 op p3))
(init op ... op pack) 二元左折叠 ((init op p1) op p2) op p3
(pack op ... op init) 二元右折叠 p1 op (p2 op (p3 op init))
  1. 对减法影响明显

  1. 所以对于 -/、赋值等非结合运算,左右折叠不能随意互换

处理空参数包

  1. 下面是一元折叠
    1. 如果参数包为空,没有任何元素可相加,因此表达式不合法

  1. 可以提供初始值
    1. 所以二元折叠可以自然处理空包

  1. 少数一元折叠对空包有标准定义
运算符 空包结果
&& true
`
, void()

用折叠表达式打印

  1. 不带分隔符

  1. 每个参数后输出空格

折叠括号不一定决定求值顺序

  1. 例如

  1. 折叠表达式决定的是括号结合方式

  1. 但不代表所有运算符的操作数都一定按从左到右求值
    1. 如果参数表达式带有副作用,不要依赖 + 的求值顺序
  2. 需要严格顺序执行时,常使用
    1. 也就是对逗号运算符折叠

  1. && 还具有短路语义
    1. 一旦前面的结果为 false,后续函数不会执行

所有参数类型是否相同

  1. 如果根据函数实参推导,需要决定是否忽略 cv 和引用

完美转发参数包

  1. 大型项目里最常见的变参模板形式是

实现简化版 invoke

  1. 参数包展开成类似
    1. 同时保持每个实参原本的左值或右值属性

  1. std::make_sharedemplace_backstd::thread 和很多 ASIO 接口都使用这种结构

线程池中保存参数包

  1. 立即调用时可以直接转发
    1. 但线程池任务会延迟执行。如果只保存引用,任务执行时调用者对象可能已经销毁
  2. 解析
    1. std::decay_t<F> 按值保存可调用对象
    2. std::make_tuple() 默认按值保存参数
    3. std::apply() 将元组重新展开为函数参数
    4. 延迟执行时不依赖调用者局部变量的引用

  1. 使用

  1. 如果确实需要引用语义,应显式使用
    1. 并确保对象生命周期覆盖任务执行时间

变参类模板

  1. 标准库中

  1. std::variant 也类似

非类型参数包

  1. 参数包不一定保存类型,也可以保存编译期值

  1. 利用索引包访问元组

变参继承:Overloaded模式

  1. 现代 C++ 项目中经常看到

  1. 假设

  1. 经常配合 std::variant

按值、const 引用与转发引用

声明 类型行为 适用场景
Args... args 复制/移动,发生 decay 函数内部需要自己的值
const Args&... args 不复制,不保留可修改性 只读打印、观察
Args&&... args 可保留值类别 转发、构造、调用包装
  1. 这会复制所有参数,并使数组退化为指针

  1. 如果要把参数继续传递给其他函数,则通常使用

依赖名称与模板消歧

什么是依赖名称

  1. 名字的含义依赖模板参数时,称为依赖名称

  1. std::size_t
    1. 无论 T 是什么,都表示同一个类型,因此是非依赖名称
  2. T::value_type
    1. 必须等到 T 确定后才能知道:
    2. value_type 是否存在
    3. 它是类型还是变量
    4. 是否可以访问
    5. 具体代表什么类型

模板的两阶段查找

  1. 第一阶段:模板定义阶段
    1. 编译器检查:
    2. 基本语法
    3. 不依赖模板参数的名称
    4. 非依赖表达式
    5. 能够立即确定的类型和函数

  1. 第二阶段:模板实例化阶段
    1. 编译器用 MyType 替换 T,然后检查object.process();是否有效

  1. 但即使某个名字要推迟查找,编译器在第一阶段仍然必须正确解析语法
    1. 模板定义阶段就必须确定
    2. 这正是需要 typename 的原因

typename:告诉编译器“这是类型”

  1. 为了告诉编译器 T::value_type 是类型,需要写

  1. 当一个限定名称同时满足:
    1. 依赖模板参数
    2. 表示一个类型
    3. 当前上下文无法自动确定它是类型

容器中的典型用法

  1. 在以下情况仍会频繁看到 typename
    1. 类型别名
    2. 返回类型
    3. 模板默认参数
    4. Allocator
    5. Traits
    6. 迭代器类型
    7. 元编程

typename 不会保证类型存在

  1. typename 只告诉编译器
    1. 按类型解析 T::value_type
    2. 它不会自动让该类型存在

  1. 同样,如果 value_type 实际是一个静态变量

哪些地方不需要或不能写 typename

  1. 非依赖类型不需要

  1. 基类列表中不能写
    1. 基类列表的位置本身要求一个类型,所以不写 typename

  1. decltype 本身已经表示类型

依赖基类为什么需要 this->

  1. Base<T> 是依赖基类,因为具体基类取决于 T
  2. 模板定义阶段,编译器通常不会在依赖基类中查找未限定名称 send
    1. 原因是 Base<T> 以后可能存在特化

  1. this 的类型依赖当前类模板特化,因此:
    1. 成为依赖名称,其查找被推迟到实例化阶段

不写 this-> 可能调用错误函数

  1. 模板定义阶段,全局 send() 可见,而依赖基类中的成员不参与普通未限定查找,于是可能绑定到::send()

this->、基类限定和 using

  1. 访问依赖基类成员有三种常见办法
  2. 使用 this->
    1. 简洁
    2. 保留正常成员查找
    3. 对虚函数保留虚派发

  1. 使用完整基类名
    1. 这种写法明确指定基类实现

  1. 使用 using
    1. using 把基类成员引入派生类作用域
    2. 当基类中存在一组重载时,这种方式尤其有用

.template:告诉编译器“这是成员模板”

  1. 假设

  1. template是消歧关键字,意思是:
    1. convert 是一个模板,后面的 <int> 是模板实参列表

三种 template 消歧形式

  1. 对象访问

  1. 指针访问

  1. 作用域访问
    1. T:::依赖限定符
    2. template:说明 Rebind 是模板
    3. <int>:模板实参
    4. typename:说明最终的 type 是类型

Allocator 中的真实模式

  1. 含义是
    1. Allocator 是模板参数
    2. allocator_traits<Allocator> 因此是依赖类型
    3. rebind_alloc 是它的成员别名模板
    4. <Node> 是成员模板实参
    5. 整个结果是一个类型

如果没有显式模板实参,通常不需要 .template

  1. 只有显式写出成员模板实参时,消歧问题最典型

示例

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

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

发表评论

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