11. 策略类与策略模式:用模板实现策略模式、策略类的组合与复用

策略模式,说白了就是把「做什么」和「怎么做」拆开。你想想看,写代码最怕什么?最怕一个函数里塞满了 if-else,今天加一种算法,明天改一种行为,改着改着就崩了。策略模式就是来解决这个问题的。

我最早接触策略模式是在一个图像处理项目里。那时候要支持多种滤镜算法,每种算法参数还不一样。一开始我用虚函数 + 继承,跑起来总觉得有点重。后来换成模板实现,编译期就把策略定死,性能直接拉满。嗯,今天我们就聊聊怎么用 C++ 模板把策略模式玩出花来。

11.1 策略模式的核心思想

策略模式定义了一族算法,把它们封装起来,让它们可以互相替换。使用策略的对象叫做「上下文」,它不关心具体策略怎么实现,只关心策略提供的接口。

传统面向对象做法是这样的:

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 Context {
    Strategy* strategy_;
public:
    explicit Context(Strategy* s) : strategy_(s) {}
    int doWork(int a, int b) { return strategy_->execute(a, b); }
};

这段代码能跑,但有个问题:每次调用 doWork 都要走虚函数表,性能有损耗。而且策略对象是运行时绑定的,你没法在编译期做优化。

核心要点:策略模式把变化的部分封装成策略,上下文只依赖策略接口。这样新增策略不用改上下文,符合开闭原则。

11.2 用模板实现编译期策略

模板可以把策略从「运行时多态」变成「编译期多态」。说白了,就是用模板参数代替虚函数。我个人的习惯是,只要策略在编译期能确定,就优先用模板。

template <typename Strategy>
class Context {
    Strategy strategy_;
public:
    explicit Context(Strategy s = Strategy{}) : strategy_(std::move(s)) {}

    int doWork(int a, int b) {
        return strategy_.execute(a, b);
    }
};

struct AddStrategy {
    int execute(int a, int b) const { return a + b; }
};

struct MulStrategy {
    int execute(int a, int b) const { return a * b; }
};

// 使用
Context<AddStrategy> ctx_add;
Context<MulStrategy> ctx_mul;

你看,这里没有虚函数,没有继承。编译器看到 Context<AddStrategy>Context<MulStrategy> 会生成两份完全不同的代码,每份都是内联展开的。性能上,跟手写 a + b 没区别。

小提示:模板策略的代价是代码膨胀。如果你的策略种类非常多(比如上百种),要考虑编译时间和二进制体积。我一般控制在 10 种以内,超过这个数就考虑运行时策略了。

11.3 策略类的组合

实际项目中,一个上下文往往需要多个策略。比如一个排序算法,可能需要比较策略和分区策略。这时候我们可以把多个策略组合起来。

我曾在写一个通用数据处理器时遇到过这种情况:既要指定数据源格式,又要指定输出格式,还要指定过滤规则。三个策略组合在一起,用模板参数包搞定:

template <typename... Strategies>
class Pipeline {
    std::tuple<Strategies...> strategies_;

public:
    explicit Pipeline(Strategies... args)
        : strategies_(std::move(args)...) {}

    template <typename T>
    T process(const T& input) {
        return std::apply([&](auto&... strat) {
            return (strat.execute(input), ...);
        }, strategies_);
    }
};

struct ReadStrategy {
    std::string execute(const std::string& raw) {
        return "read: " + raw;
    }
};

struct FilterStrategy {
    std::string execute(const std::string& data) {
        if (data.size() > 10) return data;
        return "";
    }
};

// 使用
Pipeline<ReadStrategy, FilterStrategy> pipe;
auto result = pipe.process("hello world");

这里用了 C++17 的折叠表达式和 std::apply,把多个策略串成一个流水线。每个策略的输出作为下一个策略的输入。这种组合方式非常灵活,你可以在编译期任意拼装策略链。

注意:组合策略时,要保证每个策略的输入输出类型匹配。我曾经在项目里因为类型不匹配,编译报错信息长达几百行,排查了半天。建议给每个策略加上 static_assert 做类型检查。

11.4 策略的复用与默认策略

策略类本身也可以复用。比如很多策略都有相同的「前置处理」或「后置处理」逻辑。我们可以用模板基类来提取公共部分。

template <typename Derived>
class StrategyBase {
public:
    int execute(int a, int b) {
        // 前置处理:检查参数
        if (a < 0 || b < 0) {
            throw std::invalid_argument("negative not allowed");
        }
        // 调用子类的具体实现
        return static_cast<Derived*>(this)->doExecute(a, b);
    }
};

class SafeAdd : public StrategyBase<SafeAdd> {
public:
    int doExecute(int a, int b) const { return a + b; }
};

这种 CRTP(奇异递归模板模式)的技巧,可以在不引入虚函数的前提下,给策略加上通用逻辑。我经常用它来做参数校验、日志记录、性能统计等横切关注点。

另外,给模板参数提供默认策略也是个好习惯:

template <typename Strategy = AddStrategy>
class Context {
    // ...
};

// 用户可以不传策略,默认用加法
Context<> default_ctx;

这样既保留了灵活性,又降低了使用门槛。新手可以直接用默认行为,高手可以自定义策略。

11.5 策略模式的知识体系

下面这张图总结了本章的核心脉络:

策略模式 运行时策略(虚函数) 编译期策略(模板) 组合策略(参数包) 优点:灵活,运行时切换 缺点:虚函数开销,代码分散 优点:零开销,内联优化 缺点:代码膨胀,编译慢 优点:灵活拼装,类型安全 缺点:模板元编程复杂度高 选择依据:策略数量 < 10 用模板,否则用虚函数 组合策略推荐用参数包 + 折叠表达式

11.6 避坑指南与最佳实践

讲几个我踩过的坑:

  • 模板策略不要写虚函数:既然用了编译期多态,就别混用运行时多态。混用会导致代码既膨胀又慢,两头不讨好。
  • 策略接口要稳定:一旦模板策略被多处使用,修改接口会导致所有使用者重新编译。我建议把策略接口定义成概念(C++20 Concept),这样编译报错更友好。
  • 小心策略对象的拷贝:模板策略是按值存储的,如果策略对象很大(比如包含大量状态),记得用 std::reference_wrapper 或者移动语义。

最佳实践总结

  1. 策略数量少(≤10)且编译期确定 → 模板策略
  2. 策略数量多或运行时动态加载 → 虚函数策略
  3. 多个策略需要组合 → 模板参数包 + 折叠表达式
  4. 策略需要公共逻辑 → CRTP 基类提取
  5. 给模板参数提供默认策略,降低使用门槛

嗯,策略模式用模板实现,说白了就是把「选择」从运行时提前到编译期。代价是灵活性降低,但换来的是性能提升。我个人更倾向于在性能敏感的核心路径上用模板策略,在配置灵活的外围代码上用虚函数策略。两者不冲突,看场景选就好。


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