1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

TEST(ReactionTest, TestChangeTrig) {
auto a = reaction::var(1);
auto b = reaction::var(2);
auto c = reaction::var(3);
int triggerCountA = 0;
int triggerCountB = 0;
auto ds = reaction::calc<reaction::AlwaysTrig>([&triggerCountA](int aa, double bb) { ++triggerCountA; return aa + bb; }, a, b);
auto dds = reaction::calc([&triggerCountB](auto aa, auto cc) { ++triggerCountB; return aa + cc; }, a, c);
EXPECT_EQ(triggerCountA, 1);
EXPECT_EQ(triggerCountB, 1);
a.value(1);
EXPECT_EQ(triggerCountA, 2);
EXPECT_EQ(triggerCountB, 1);

a.value(2);
EXPECT_EQ(triggerCountA, 3);
EXPECT_EQ(triggerCountB, 2);
}

TEST(ReactionTest, TestFilterTrig) {
auto a = reaction::var(1);
auto b = reaction::var(2);
auto c = reaction::var(3);
auto ds = reaction::calc([](int aa, double bb) { return aa + bb; }, a, b);
auto dds = reaction::calc<reaction::FilterTrig>([](auto cc, auto dsds) { return cc + dsds; }, c, ds);
*a = 2;
EXPECT_EQ(ds.get(), 4);
EXPECT_EQ(dds.get(), 7);

dds.filter([&]() { return c() + ds() < 10; });
*a = 3;
EXPECT_EQ(dds.get(), 8);

*a = 5;
EXPECT_EQ(dds.get(), 8);
}

TEST(ReactionTest, TestCloseStra) {
auto a = reaction::var(1).setName("a");
auto b = reaction::var(2).setName("b");

auto dsB = reaction::calc([](auto aa) { return aa; }, a).setName("dsB");
auto dsC = reaction::calc([](auto aa) { return aa; }, a).setName("dsC");
auto dsD = reaction::calc([](auto aa) { return aa; }, a).setName("dsD");
auto dsE = reaction::calc([](auto aa) { return aa; }, a).setName("dsE");
auto dsF = reaction::calc([](auto aa) { return aa; }, a).setName("dsF");
auto dsG = reaction::calc([](auto aa) { return aa; }, a).setName("dsG");

{
auto dsA = reaction::calc<reaction::ChangeTrig, reaction::CloseStra>([](int aa) { return aa; }, a).setName("dsA");
dsB.reset([&]() { return a() + dsA(); });
dsC.reset([&]() { return a() + dsA() + dsB(); });
dsD.reset([&]() { return dsA() + dsB() + dsC(); });
dsE.reset([&]() { return dsB() * dsC() + dsD(); });
dsF.reset([&]() { return a() + b(); });
dsG.reset([&]() { return dsA() + dsF(); });
}

EXPECT_FALSE(static_cast<bool>(dsB));
EXPECT_FALSE(static_cast<bool>(dsC));
EXPECT_FALSE(static_cast<bool>(dsD));
EXPECT_FALSE(static_cast<bool>(dsE));
EXPECT_TRUE(static_cast<bool>(dsF));
EXPECT_FALSE(static_cast<bool>(dsG));
}

TEST(ReactionTest, TestKeepStra) {
auto a = reaction::var(1).setName("a");

auto dsB = reaction::calc([](auto aa) { return aa; }, a).setName("dsB");
auto dsC = reaction::calc([](auto aa) { return aa; }, a).setName("dsC");

{
auto dsA = reaction::calc([](int aa) { return aa; }, a).setName("dsA");

dsB.reset([](int aa, int AA) { return aa + AA; }, a, dsA);
dsC.reset([](int aa, int AA, int BB) { return aa + AA + BB; }, a, dsA, dsB);
}

EXPECT_EQ(dsB.get(), 2);
EXPECT_EQ(dsC.get(), 4);

a.value(10);
EXPECT_EQ(dsB.get(), 20);
EXPECT_EQ(dsC.get(), 40);
}

TEST(ReactionTest, TestLastStra) {
auto a = reaction::var(1).setName("a");

auto dsB = reaction::calc([](auto aa) { return aa; }, a).setName("dsB");
auto dsC = reaction::calc([](auto aa) { return aa; }, a).setName("dsC");

{
auto dsA = reaction::calc<reaction::ChangeTrig, reaction::LastStra>([](int aa) { return aa; }, a).setName("dsA");

dsB.reset([](int aa, int AA) { return aa + AA; }, a, dsA);
dsC.reset([](int aa, int AA, int BB) { return aa + AA + BB; }, a, dsA, dsB);
}

EXPECT_EQ(dsB.get(), 2);
EXPECT_EQ(dsC.get(), 4);

a.value(10);
EXPECT_EQ(dsB.get(), 11);
EXPECT_EQ(dsC.get(), 22);
}

第九章:触发模式的实现

💡 在实现触发模式之前,先聊聊策略模式(Strategy Pattern)

策略模式的核心思想是:

将可变的行为从类中抽离出来,以便在运行时灵活替换策略。

那么,如何在一个类中配置不同策略并执行它呢?常见有三种实现方式:


1. 枚举分支法(最直接但最笨)

定义一个枚举成员变量表示当前策略类型,通过 setStrategyType() 来切换,并在执行时使用 if-elseswitch-case 判断:

1
2
3
4
5
6
7
8
9
enum class StrategyType { A, B, C };

void execute() {
if (type == StrategyType::A) {
// ...
} else if (type == StrategyType::B) {
// ...
}
}

缺点:

  • 扩展性差,每加一个策略都要改 if-else
  • 多一个成员变量,带来额外内存。

2. 虚函数多态法(面向对象做法)

定义一个策略基类,所有策略继承该类并重写虚函数接口,通过指针或智能指针注入策略对象:

1
2
3
4
5
6
7
8
9
10
11
struct Strategy {
virtual void execute() = 0;
virtual ~Strategy() = default;
};

class ConcreteA : public Strategy {
public:
void execute() override {
// ...
}
};

优点:

  • 扩展性好,新增策略只需新增子类,不改现有逻辑。

缺点:

  • 引入虚函数的运行时开销。
  • 必须使用指针或引用管理对象生命周期。

✅ 什么是静态多态(Static Polymorphism)

静态多态是指在编译期决定调用哪个函数版本的多态行为,通常通过模板实现。

对比动态多态:

类型 实现方式 多态决议时间 开销 可内联
动态多态 虚函数 + 指针/引用 运行时 虚表开销
静态多态 模板 / CRTP 编译时 零开销 ✅ 可内联

🌟 实现静态多态的两种常见方式


✅ 方式一:模板参数接口(Duck Typing)

策略类只要包含某个函数名就可以作为模板参数传入使用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename TriggerPolicy>
class Node {
public:
void update(double newVal) {
if (TriggerPolicy{}.checkTrigger(newVal)) {
std::cout << "Triggered!\n";
}
}
};

struct AlwaysTrigger {
bool checkTrigger(double) { return true; }
};

Node<AlwaysTrigger> node; // 编译期绑定
node.update(42); // 输出 Triggered!

✅ 优点:

  • 结构简单,零运行时开销;
  • 不需要继承和虚函数;
  • 编译期保证接口存在。

❌ 缺点:

  • 错误信息可读性差(如果函数名写错);
  • 不能在运行时替换策略。

✅ 方式二:CRTP(Curiously Recurring Template Pattern)

CRTP 是一种让子类通过模板参数传递给父类,实现“静态的接口调用”。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};

class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived impl\n";
}
};

Derived d;
d.interface(); // 输出 Derived impl

✅ 优点:

  • 可构建静态接口体系,类似虚函数但无虚表;
  • 支持接口层逻辑共享(在 Base 中定义默认逻辑);
  • 在编译期完成派发,性能优异。

🚀 静态多态的典型应用场景

场景 描述
策略模式 如你提供的 Trigger / Strategy 接口模式
数值计算框架 如 Eigen / Blaze / Tensor 模板表达式
自定义容器 / 算法 如 STL 算法用 CompareAllocator 等模板类型
CRTP 接口设计 可为派生类提供静态的“虚接口”体系
模板元编程 / Policy 大型系统中常用政策类来配置行为,如 Boost.Spirit

🧠 对比动态多态(虚函数)的设计取舍

维度 静态多态(模板) 动态多态(虚函数)
决议时间 编译期 运行时
性能 极高,无虚表,无额外内存访问 较低,虚表跳转,有缓存 miss 风险
灵活性 模板固定,运行时不可切换策略 可以运行时替换子类实例
接口约束 鸭子类型,易出错 明确接口(基类定义),类型安全
错误提示 编译期但复杂,可读性差 运行时报错少,但类型明确
应用场景 高性能需求、底层框架、可组合策略 插件系统、脚本引擎、接口稳定性优先

从默认模板参数谈到别名模板和变量模板的本质:

先看下面一段代码:
https://godbolt.org/z/hrE4Tn9Gr

关键区别解析

1. 函数模板的特殊性(func2 为何能通过)

  • 参数推导机制:函数模板可以通过函数参数推导缺失的模板参数。
    1
    2
    3
    4
    template<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
    3
    Alia2<double> obj; // 歧义:
    // 1. T1=double, T2=? (无默认值)
    // 2. T1=int (默认), T2=double?
    编译器无法解析这种歧义,因此强制要求默认参数必须从右向左连续。

根本原因:实例化机制差异

特性 函数模板 类模板/别名模板/变量模板
参数推导 支持(通过函数参数) 不支持
实例化触发 调用时推导 显式指定类型时实例化
默认参数规则 允许非连续(依赖推导补全) 必须从右向左连续
错误触发时机 调用时(若无法推导) 声明时(违反连续规则)

将别名模板(alias template)和变量模板(variable template)描述为”语法糖”是一个很恰当的比喻。

什么是”语法糖”?

在编程语言中,”语法糖”指的是:

  1. 不引入新功能,只是提供更简洁、更易读的语法
  2. 在编译时会转换为更基础的语法结构
  3. 使代码更符合人类直觉的表达方式
  4. 不改变底层执行逻辑或性能特征

别名模板作为语法糖

本质:别名模板是类模板特化的简写形式

示例对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 基础类模板
template<typename T>
class MyVector {
// 复杂实现...
};

// 传统使用方式(冗长)
MyVector<int> v1;
MyVector<double> v2;

// 别名模板(语法糖)
template<typename T>
using Vec = MyVector<T>;

// 简化后的使用
Vec<int> v3;
Vec<double> v4;

编译时转换

  • Vec<int> 在编译时直接替换为 MyVector<int>
  • 不生成新类型,只是现有类型的别名
  • 类型特征检查时等同于原始类型:
    1
    static_assert(std::is_same_v<Vec<int>, MyVector<int>>); // true

变量模板作为语法糖

本质:变量模板是类模板静态成员的简写形式

示例对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 传统方式:通过类模板的静态成员
template<typename T>
struct Constants {
static constexpr T PI = T(3.1415926535);
};

// 使用方式(冗长)
double circle = Constants<double>::PI * r * r;

// 变量模板(语法糖)
template<typename T>
constexpr T PI = T(3.1415926535);

// 简化后的使用
double circle = PI<double> * r * r;

编译时转换

  • PI<double> 直接替换为 Constants<double>::PI
  • 不创建新实体,只是现有静态成员的别名访问
  • 地址相同证明是同一实体:
    1
    assert(&PI<double> == &Constants<double>::PI);

为什么它们需要遵守类模板的规则?

虽然别名模板和变量模板是语法糖,但它们的实现依赖于底层类模板机制:

  1. 类型系统一致性

    • 别名模板最终指向类模板特化
    • 必须遵守类模板的实例化规则
    • 包括默认参数必须从右向左连续
  2. 实例化机制

    1
    2
    template<typename T = void> // 错误:非连续默认
    using ErrorProne = std::vector<T>;
    • 实例化时 ErrorProne<> 无法确定类型
    • 因为底层 std::vector<> 要求明确指定元素类型
  3. 元编程特性

    • 在模板元编程中,别名模板参与类型计算
    • 必须保持与类模板相同的特化规则
    • 例如偏特化传播:
      1
      2
      3
      4
      5
      6
      7
      8
      template<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. 可读性提升

    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;
  2. 减少样板代码

    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;
  3. API 简化

    1
    2
    3
    // 变量模板简化标准库
    template<class T>
    inline constexpr bool is_pointer_v = std::is_pointer<T>::value;

总结

  1. 别名模板和变量模板确实是语法糖:

    • 编译时展开为底层类模板结构
    • 不创建新类型或新存储实体
    • 提供更简洁的访问接口
  2. 但必须遵守类模板规则:

    • 因为它们直接映射到类模板特化
    • 类型系统要求一致性
    • 实例化机制依赖底层模板
  3. 这种设计是深思熟虑的:

    • 保持类型系统一致性
    • 避免引入新规则导致的复杂性
    • 在零开销抽象下提供语法便利