18、策略模式(Policy-Based Design):模板参数作为策略、编译期选择算法

策略模式,说白了就是把「怎么做」和「谁来做」彻底分开。运行时多态我们见得多了——基类指针、虚函数、动态绑定。但模板元编程给了我们另一条路:编译期策略选择。这条路没有虚表开销,没有间接调用,代码直接内联展开。

我个人习惯把这种设计叫做「零成本抽象」的典范。你想想看,运行时多态要付出多少代价?每次调用都要查虚表,分支预测还可能失败。而编译期策略,编译器直接帮你把代码焊死在调用点上。

策略模式的核心思想

策略模式(Policy-Based Design)最早由 Andrei Alexandrescu 在《Modern C++ Design》中系统阐述。它的核心就一句话:用模板参数来传递行为

传统的策略模式长这样:

// 运行时策略
class Strategy {
public:
    virtual int execute(int a, int b) = 0;
    virtual ~Strategy() = default;
};

class AddStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a + b; }
};

class MulStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a * b; }
};

// 使用时需要动态绑定
void compute(Strategy& s, int x, int y) {
    std::cout << s.execute(x, y) << std::endl;
}

嗯,这里有个问题。每次调用 s.execute 都要走虚表。如果这个函数被频繁调用,性能损失不可忽视。我在项目中遇到过类似场景——一个图像处理管线,每个像素都要经过策略选择,虚函数调用直接让帧率掉了 30%。

换成编译期策略呢?

// 编译期策略
struct AddPolicy {
    static int execute(int a, int b) { return a + b; }
};

struct MulPolicy {
    static int execute(int a, int b) { return a * b; }
};

template <typename Policy>
void compute(int x, int y) {
    std::cout << Policy::execute(x, y) << std::endl;
}

// 使用
compute<AddPolicy>(3, 4);  // 7
compute<MulPolicy>(3, 4);  // 12

看到区别了吗?Policy::execute 是静态调用,编译器可以直接内联。生成的汇编代码里,连函数调用的痕迹都找不到。

更复杂的策略组合

实际项目中,策略往往不止一个维度。比如一个内存分配器,可能需要同时指定分配策略、回收策略、线程安全策略。用模板参数组合起来:

template <typename AllocPolicy, typename ReclaimPolicy, typename ThreadPolicy>
class MemoryPool {
public:
    void* allocate(size_t size) {
        // 使用 AllocPolicy 分配
        void* ptr = AllocPolicy::allocate(size);
        // 使用 ThreadPolicy 加锁
        ThreadPolicy::lock();
        // ...
        return ptr;
    }

    void deallocate(void* ptr) {
        // 使用 ReclaimPolicy 回收
        ReclaimPolicy::reclaim(ptr);
        ThreadPolicy::unlock();
    }
};

我曾经在写一个高性能服务器时用过这种设计。不同的业务场景需要不同的内存管理策略:

  • 高频交易:用固定大小池 + 无锁策略
  • 流媒体:用变长分配 + 延迟回收
  • 日志系统:用简单 malloc + 线程局部缓存

通过模板参数组合,我只需要写一个 MemoryPool 类,然后实例化出不同的版本:

using FastPool = MemoryPool<FixedAlloc<64>, NoReclaim, LockFreePolicy>;
using StreamPool = MemoryPool<VarAlloc, DelayedReclaim, MutexPolicy>;
using LogPool = MemoryPool<MallocAlloc, ImmediateReclaim, ThreadLocalPolicy>;

每个实例都是完全独立的类型,编译器会为每个组合生成最优代码。没有虚函数,没有运行时判断,只有纯粹的编译期展开。

策略类的默认参数与特化

写策略类时,我建议给常用策略提供默认值。这样用户只需要关注他们想定制的部分:

template <typename T,
          typename AllocPolicy = DefaultAlloc<T>,
          typename ComparePolicy = DefaultCompare<T>>
class Container {
    // ...
};

用户如果只需要改比较策略,可以这样写:

Container<int, DefaultAlloc<int>, ReverseCompare<int>> c;

嗯,这里要注意。模板参数多了以后,代码会变得冗长。C++17 的类模板参数推导(CTAD)能缓解一部分,但策略类通常需要显式指定。我个人习惯用 using 别名来简化:

template <typename T>
using ReverseContainer = Container<T, DefaultAlloc<T>, ReverseCompare<T>>;

编译期策略的约束与检查

策略模式用多了,你会发现一个问题:用户传进来的策略类可能不满足要求。比如你的策略类需要有一个 static void init() 方法,但用户传了一个没有这个方法的类。编译错误会非常难懂。

我建议用 static_assert 加上类型萃取来提前检查:

template <typename Policy>
class Widget {
    static_assert(std::is_same_v<decltype(Policy::init()), void>,
                  "Policy must provide a static init() method returning void");
    // ...
};

如果你用的是 C++20,可以用 concept 来约束:

template <typename P>
concept PolicyConcept = requires {
    { P::init() } -> std::same_as<void>;
    { P::process(int{}) } -> std::same_as<int>;
};

template <PolicyConcept Policy>
class Widget {
    // ...
};

这样用户传错策略时,编译器给出的错误信息会清晰得多。我曾经在一个大型项目中因为策略不匹配,花了整整一天才定位到问题。从那以后,我每个策略类都加了 concept 约束。

策略模式 vs 运行时多态

什么时候用编译期策略,什么时候用运行时多态?我总结了一个简单的判断标准:

场景 推荐方式 原因
策略在编译期已知 编译期策略 零开销,内联优化
策略需要动态加载 运行时多态 无法在编译期确定
策略数量少且固定 编译期策略 代码膨胀可控
策略数量多且变化频繁 运行时多态 避免模板爆炸
对性能极度敏感 编译期策略 消除间接调用
需要二进制兼容性 运行时多态 ABI 稳定

说白了,编译期策略适合「我知道我要什么,而且我要最快」的场景。运行时多态适合「我不知道用户会传什么,但我得兼容」的场景。

策略模式的核心逻辑图

下面这张图展示了策略模式在编译期和运行时的对比:

策略模式:编译期 vs 运行时 编译期策略(模板) AddPolicy MulPolicy template<typename Policy> class Calculator { ... }; Calculator<AddPolicy> Calculator<MulPolicy> ✅ 内联展开,零开销 ✅ 内联展开,零开销 运行时多态(虚函数) class Strategy (虚基类) AddStrategy MulStrategy 虚表指针 → vtable 虚表指针 → vtable ⚠ 虚表查找 + 间接调用 ⚠ 虚表查找 + 间接调用 vs

核心要点:编译期策略通过模板参数将行为注入类中,所有决策在编译期完成。运行时多态通过虚函数表在运行时动态分发。前者性能更好,后者灵活性更高。

个人建议:如果你在写库或者框架,优先考虑编译期策略。用户可以通过模板参数定制行为,而且编译器会帮你做优化。如果你在写应用层代码,且策略需要动态切换,运行时多态更合适。

避坑指南:我曾经在一个项目中过度使用策略模式,一个类有 6 个模板参数,每个参数有 3-4 种选择。结果编译出来的二进制文件膨胀了 3 倍。策略模式虽好,但不要滥用。一般建议策略参数不超过 3 个,每个策略的选择不超过 5 种。

总结

策略模式(Policy-Based Design)是模板元编程中非常实用的技术。它把行为抽象成模板参数,让编译器在编译期完成策略选择。相比运行时多态,它没有虚表开销,代码可以完全内联。但代价是代码膨胀和编译时间增加。

我个人在写高性能组件时几乎必用策略模式。内存池、线程池、序列化器、算法适配器……这些场景下,编译期策略带来的性能提升非常明显。但我也吃过亏——策略参数太多导致代码难以维护。所以我的建议是:适度使用,明确边界,该用运行时多态的时候别硬上模板

公众号:蓝海资料掘金营,微信 deep3321