第九章:触发模式的实现
1 |
|
第九章:触发模式的实现
💡 在实现触发模式之前,先聊聊策略模式(Strategy Pattern)
策略模式的核心思想是:
将可变的行为从类中抽离出来,以便在运行时灵活替换策略。
那么,如何在一个类中配置不同策略并执行它呢?常见有三种实现方式:
1. 枚举分支法(最直接但最笨)
定义一个枚举成员变量表示当前策略类型,通过 setStrategyType()
来切换,并在执行时使用 if-else
或 switch-case
判断:
1 | enum class StrategyType { A, B, C }; |
缺点:
- 扩展性差,每加一个策略都要改
if-else
。 - 多一个成员变量,带来额外内存。
2. 虚函数多态法(面向对象做法)
定义一个策略基类,所有策略继承该类并重写虚函数接口,通过指针或智能指针注入策略对象:
1 | struct Strategy { |
优点:
- 扩展性好,新增策略只需新增子类,不改现有逻辑。
缺点:
- 引入虚函数的运行时开销。
- 必须使用指针或引用管理对象生命周期。
✅ 什么是静态多态(Static Polymorphism)
静态多态是指在编译期决定调用哪个函数版本的多态行为,通常通过模板实现。
对比动态多态:
类型 | 实现方式 | 多态决议时间 | 开销 | 可内联 |
---|---|---|---|---|
动态多态 | 虚函数 + 指针/引用 | 运行时 | 虚表开销 | 否 |
静态多态 | 模板 / CRTP | 编译时 | 零开销 | ✅ 可内联 |
🌟 实现静态多态的两种常见方式
✅ 方式一:模板参数接口(Duck Typing)
策略类只要包含某个函数名就可以作为模板参数传入使用。
示例:
1 | template<typename TriggerPolicy> |
✅ 优点:
- 结构简单,零运行时开销;
- 不需要继承和虚函数;
- 编译期保证接口存在。
❌ 缺点:
- 错误信息可读性差(如果函数名写错);
- 不能在运行时替换策略。
✅ 方式二:CRTP(Curiously Recurring Template Pattern)
CRTP 是一种让子类通过模板参数传递给父类,实现“静态的接口调用”。
示例:
1 | template<typename Derived> |
✅ 优点:
- 可构建静态接口体系,类似虚函数但无虚表;
- 支持接口层逻辑共享(在 Base 中定义默认逻辑);
- 在编译期完成派发,性能优异。
🚀 静态多态的典型应用场景
场景 | 描述 |
---|---|
策略模式 | 如你提供的 Trigger / Strategy 接口模式 |
数值计算框架 | 如 Eigen / Blaze / Tensor 模板表达式 |
自定义容器 / 算法 | 如 STL 算法用 Compare 、Allocator 等模板类型 |
CRTP 接口设计 | 可为派生类提供静态的“虚接口”体系 |
模板元编程 / Policy | 大型系统中常用政策类来配置行为,如 Boost.Spirit |
🧠 对比动态多态(虚函数)的设计取舍
维度 | 静态多态(模板) | 动态多态(虚函数) |
---|---|---|
决议时间 | 编译期 | 运行时 |
性能 | 极高,无虚表,无额外内存访问 | 较低,虚表跳转,有缓存 miss 风险 |
灵活性 | 模板固定,运行时不可切换策略 | 可以运行时替换子类实例 |
接口约束 | 鸭子类型,易出错 | 明确接口(基类定义),类型安全 |
错误提示 | 编译期但复杂,可读性差 | 运行时报错少,但类型明确 |
应用场景 | 高性能需求、底层框架、可组合策略 | 插件系统、脚本引擎、接口稳定性优先 |
从默认模板参数谈到别名模板和变量模板的本质:
先看下面一段代码:
https://godbolt.org/z/hrE4Tn9Gr
关键区别解析
1. 函数模板的特殊性(func2
为何能通过)
- 参数推导机制:函数模板可以通过函数参数推导缺失的模板参数。
1
2
3
4template<typename T1 = int, typename T2>
void func2(T2 a) { /* ... */ } // T2 通过实参推导
func2(42); // T2 被推导为 int, T1 使用默认 int - 非连续默认参数允许:只要调用时能推导出所有非默认参数,即使默认参数在左侧(如
T1
)也能编译。
2. 类模板/别名模板的严格性(Alia2
为何报错)
- 无参数推导机制:类模板、别名模板和变量模板没有调用时的参数推导,必须显式指定所有参数或依赖默认值。
- 从右向左连续规则:
1
2
3
4
5
6
7// 合法:默认参数从右向左连续
template<typename T1, typename T2 = int>
using Alia1 = Obj<T1, T2>;
// 非法:T1 有默认值但 T2 无默认值
template<typename T1 = int, typename T2>
using Alia2 = Obj<T1, T2>; // 错误! - 实例化依赖显式指定:
若允许Alia2
存在,则实例化时无法确定如何省略参数:编译器无法解析这种歧义,因此强制要求默认参数必须从右向左连续。1
2
3Alia2<double> obj; // 歧义:
// 1. T1=double, T2=? (无默认值)
// 2. T1=int (默认), T2=double?
根本原因:实例化机制差异
特性 | 函数模板 | 类模板/别名模板/变量模板 |
---|---|---|
参数推导 | 支持(通过函数参数) | 不支持 |
实例化触发 | 调用时推导 | 显式指定类型时实例化 |
默认参数规则 | 允许非连续(依赖推导补全) | 必须从右向左连续 |
错误触发时机 | 调用时(若无法推导) | 声明时(违反连续规则) |
将别名模板(alias template)和变量模板(variable template)描述为”语法糖”是一个很恰当的比喻。
什么是”语法糖”?
在编程语言中,”语法糖”指的是:
- 不引入新功能,只是提供更简洁、更易读的语法
- 在编译时会转换为更基础的语法结构
- 使代码更符合人类直觉的表达方式
- 不改变底层执行逻辑或性能特征
别名模板作为语法糖
本质:别名模板是类模板特化的简写形式
示例对比:
1 | // 基础类模板 |
编译时转换:
Vec<int>
在编译时直接替换为MyVector<int>
- 不生成新类型,只是现有类型的别名
- 类型特征检查时等同于原始类型:
1
static_assert(std::is_same_v<Vec<int>, MyVector<int>>); // true
变量模板作为语法糖
本质:变量模板是类模板静态成员的简写形式
示例对比:
1 | // 传统方式:通过类模板的静态成员 |
编译时转换:
PI<double>
直接替换为Constants<double>::PI
- 不创建新实体,只是现有静态成员的别名访问
- 地址相同证明是同一实体:
1
assert(&PI<double> == &Constants<double>::PI);
为什么它们需要遵守类模板的规则?
虽然别名模板和变量模板是语法糖,但它们的实现依赖于底层类模板机制:
类型系统一致性:
- 别名模板最终指向类模板特化
- 必须遵守类模板的实例化规则
- 包括默认参数必须从右向左连续
实例化机制:
1
2template<typename T = void> // 错误:非连续默认
using ErrorProne = std::vector<T>;- 实例化时
ErrorProne<>
无法确定类型 - 因为底层
std::vector<>
要求明确指定元素类型
- 实例化时
元编程特性:
- 在模板元编程中,别名模板参与类型计算
- 必须保持与类模板相同的特化规则
- 例如偏特化传播:
1
2
3
4
5
6
7
8template<typename T>
struct is_int : std::false_type {};
template<>
struct is_int<int> : std::true_type {};
template<typename T>
using IsInt = is_int<T>; // 继承所有特化
关键区别:语法糖 vs 新实体
特性 | 类模板 | 别名模板/变量模板 |
---|---|---|
创建新类型/实体 | 是 | 否(仅是别名) |
内存占用 | 可能产生新实例化 | 零开销 |
ODR 使用 | 需要完整定义 | 仅需声明 |
默认参数规则 | 从右向左连续 | 继承相同规则 |
模板特化 | 支持全特化/偏特化 | 只能基于现有特化 |
实际价值
虽然本质是语法糖,但它们提供了重大价值:
可读性提升:
1
2
3
4
5
6
7
8// 未使用别名模板
std::map<std::string, std::vector<std::pair<int, double>>> complexType;
// 使用别名模板
template<typename K, typename V>
using Dictionary = std::map<K, std::vector<std::pair<int, V>>>;
Dictionary<std::string, double> readableType;减少样板代码:
1
2
3
4
5
6
7
8
9// 传统方式
template<typename T>
struct Identity {
using type = T;
};
// 使用别名模板
template<typename T>
using Identity_t = typename Identity<T>::type;API 简化:
1
2
3// 变量模板简化标准库
template<class T>
inline constexpr bool is_pointer_v = std::is_pointer<T>::value;
总结
别名模板和变量模板确实是语法糖:
- 编译时展开为底层类模板结构
- 不创建新类型或新存储实体
- 提供更简洁的访问接口
但必须遵守类模板规则:
- 因为它们直接映射到类模板特化
- 类型系统要求一致性
- 实例化机制依赖底层模板
这种设计是深思熟虑的:
- 保持类型系统一致性
- 避免引入新规则导致的复杂性
- 在零开销抽象下提供语法便利
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.