c++20模板元编程
tips
- 主模板 + 偏特化
- AKA 定义默认行为
- 类型擦除
- 奇异递归
- 模板表达式
- 标签派发
- 静态面向”对象”
- 静态多态:
- 奇异递归
- 鸭子类型: e.g. golang中的interface
- 混入(mixin): 静态检查
- 类型擦除, 没有多态的通用处理(但有相同的interface)
- 静态多态:
变参模板
...运算符表示解包, 在什么后面就是对什么的解包template<typename... T>func(T... args): 对形参T解包, 其中T...称为形参包call_func(args...): 对实参args解包
- 变参目标可以匹配多个形参包: 贪婪匹配: 直到形参包无法匹配是才推导下一个
1 | template<typename... Ts, typename... Us> |
变参类
e.g. tuple
1 | template<typename T, typename... Ts> |
e.g. get<N>
1 | template<int N, typename T, typename... Ts> |
折叠表达式
e.g.
1 | template<typename... Ts> |
4种折叠: ...在pack的左边还是右边
- 无init: 一元折叠
- 一元右折叠:
pack op ...- (arg1 op (arg2 op (arg3 op argN)))
- 越靠右结合律越高
- 一元左折叠:
... op pack- (((arg1 op arg2) op arg3) op argN)
- 越靠左结合律越高
- 一元右折叠:
- 有init: 二元折叠, 结合规律同理一元, 区别在于init要cat在左边还是右边, 拼接init后做一元折叠
- 二元右折叠:
pack op ... op init- (arg1 op (arg2 op (arg3 op (argN op init)))
- 二元左折叠:
init op ... op pack- ((((init op arg1) op arg2) op arg3) op argN)
- 二元右折叠:
移动语义
in short. 左值: “有存储位置”, 右值: “临时的, 没有存储位置, 在调用之外不存在”
完美转发: 因为左值引用的实参本身也是一个左值, 所以对这个实参进一步传递时只会调用左值的重载。
1 | void g(int& v) { std::cout << "g(int&)\n"; } |
declval
std::declval<T>()生成一个类型T的值(对象), 且: 是右值引用 + 无需调用构造函数。
e.g. 为了解决如下问题: 需要值(对象)来做推导, 但是没有默认构造
1 | template <typename T, typename U> |
条件编译
主模板 + 偏特化: 偏特化失败就会落到主模板。e.g.
1 | template<typename T> |
往往搭配其他模板使用, e.g. 一个true特化的, 一个false特化的。
SFINAE
Substitution failure is not an error(替换失败不是错误)
AKA模板的匹配机制: 不匹配(替换失败)就尝试其他的替换
“自动条件编译”: 哪个匹配用哪个
TODO: review
条件编译
SFINAE会调一个匹配的实现。当匹配到B=false的实现时, 在使用时就会编译报错。
1 | template <bool B, typename T = void> |
some how等价于
1 | if constexpr (true) { |
type trait
类型特征
“用模板的方式加各种限定” + 别名/工具包
概念和 requires
背景: 只查看声明不检查主体无法知道确定的功能(比如可能被奇怪的重载了)
目的: 增加可读性
requires用法: “类似enable_if, 但是报错更友好”
1 | template <typename T> |
concept用法: “类似using, 是typename + requires的语法糖”
1 | template <typename T> |
简单要求
根据表达式正确的确定true/false
编译器会检查是否实现了+
1 | template <typename T> |
类型要求
约束
<typename T>的T的简单要求
e.g. 一个类型必须包含内部类型key_type和value_type
1 | template <typename T> |
用法同理requires
1 | template <KVP T> |
符合要求
要求含有某个方法和返回类型
1 | template <typename T> |
嵌套要求和组合要求
1 | template <typename T> |
静态多态
模板自动生成多态的具体实现
e.g. t.value++有具体的类型实现(具有多态性)
1 | struct attack { |
奇异递归模板模式
CRTP
AKA静态多态 + 面向对象, 可以节省查虚表的开销
模式如下:
- 有一个定义接口的基类类模板(静态)
- 派生类本身是基类模板的实参
- 基类函数调用派生类函数
tips: 因为都实现了统一的接口, 所以通过基类方式统一调用就行。模板负责生成不同的重载。
1 | template <typename T> |
混入
mixin
编译期功能组合, 编译器检查有没有实现所需的功能
AKA鸭子类型, 只要有某个interface就行
e.g. 可以检查出T有没有都实现所需的功能
1 | many_funcs<T> k; |
类型擦除
通用方式处理不一定相关的类型。e.g. 没有继承/多态关系也能通用处理
- 无继承/多态关系的通用处理, 但是鸭子类型
void*+ 类型转换是一种方法, 但类型不安全
- 思想: 创建一个通用的间接类, 该类内具体的对象实例由模板生成
- A和B没有直接的关系, 但有相同的接口
- 为了不改变A, B之间的关系, 可以试想一个间接的连接
- e.g.
concept_unit_for_A,concept_unit_for_B, 他们都继承了unit, 有相同的接口, 但内部实例化的对象分别是A, B
- e.g.
- 用间接类做通用操作即可
- 套路
- 定义一个concept, 表示共有的接口
struct concept{}
- 定义一个concept, 表示共有的接口
- 模板定义一个间接类, 间接类都继承了concept
template<class T> struct _base: concept{}
- 模板定义一个间接类, 间接类都继承了concept
- 使用时对外暴露操作的其实是间接类, 模板规划会生成对应的示例
_base.inferface()
- 使用时对外暴露操作的其实是间接类, 模板规划会生成对应的示例
- fwd语义 + 智能指针
e.g.
1 | struct unit { |
标签派发
编译期函数选择(或者重载选择). e.g. 容器类型
- wrapper函数内部通过trait决定调用哪种实现
- 替代方案
if constexpr- concept约束
1 | template <typename Iter, typename Distance> |