一、Policy-Based 设计:用模板参数做配置策略

Policy-Based 设计,说白了就是「把行为当作参数传进去」。

我最早接触这个思想是在写一个内存池的时候。当时项目里需要支持多种分配策略——有的场景要快,有的场景要安全,有的场景要统计。如果每个场景都写一个类,代码量爆炸。后来我用了 Policy 设计,一个模板类搞定所有场景。

嗯,今天我们就来聊聊这个。

1.1 什么是 Policy-Based 设计?

Policy-Based 设计的核心思想是:把类的行为拆分成多个策略(Policy),每个策略用一个模板参数表示。使用者通过组合不同的策略,得到不同的类。

举个例子:

template <typename ThreadPolicy, typename LockPolicy>
class DataBuffer {
    // 使用 ThreadPolicy 决定是否线程安全
    // 使用 LockPolicy 决定锁的类型
};

你看,这个 DataBuffer 本身不关心线程安全怎么实现。它只负责调用 ThreadPolicy 提供的接口。具体是单线程、多线程、读写锁,全由模板参数决定。

核心思想: 将「做什么」和「怎么做」分离。类只定义「做什么」,Policy 定义「怎么做」。

1.2 为什么需要 Policy-Based 设计?

我遇到过不少团队,代码里充斥着这样的写法:

// 方案A:用宏控制
#ifdef USE_THREAD_SAFE
    class DataBuffer { /* 加锁版本 */ };
#else
    class DataBuffer { /* 不加锁版本 */ };
#endif

// 方案B:用继承
class ThreadSafeBuffer : public DataBuffer { /* 重写加锁方法 */ };
class SingleThreadBuffer : public DataBuffer { /* 空实现 */ };

这两种方案都有问题。宏方案让代码难以阅读和维护,继承方案则会产生大量子类。你想想看,如果策略有 3 个维度,每个维度有 2 种选择,继承方案就要写 8 个类。

Policy-Based 设计用模板组合,只需要 1 个模板类 + 6 个 Policy 实现,使用者自由组合:

using SafeBuffer = DataBuffer<MultiThread, SpinLock>;
using FastBuffer = DataBuffer<SingleThread, NoLock>;

干净、清晰、可扩展。

1.3 Policy 的基本结构

一个 Policy 通常是一个类或类模板,它定义了一组接口约定。Policy 本身不单独使用,而是作为模板参数注入到宿主类中。

来看一个完整的例子:

// 定义 Policy:分配策略
struct MallocAlloc {
    static void* allocate(size_t size) {
        return malloc(size);
    }
    static void deallocate(void* p) {
        free(p);
    }
};

struct PoolAlloc {
    static void* allocate(size_t size) {
        return pool_alloc(size);  // 假设有池分配器
    }
    static void deallocate(void* p) {
        pool_free(p);
    }
};

// 宿主类
template <typename AllocPolicy>
class Container {
public:
    void* getBuffer(size_t size) {
        return AllocPolicy::allocate(size);
    }
    void releaseBuffer(void* p) {
        AllocPolicy::deallocate(p);
    }
};

// 使用
using FastContainer = Container<PoolAlloc>;
using SimpleContainer = Container<MallocAlloc>;

小技巧: Policy 通常使用静态方法,这样不需要创建 Policy 对象。如果 Policy 需要状态,也可以使用非静态方法,但要注意生命周期管理。

1.4 Policy 的接口约定

Policy 的接口约定是隐式的——通过模板实例化时的编译检查来保证。如果某个 Policy 没有提供宿主类要求的接口,编译就会报错。

我个人习惯在文档中明确写出 Policy 需要满足的接口:

接口 说明 返回值
allocate(size) 分配指定大小的内存 void*
deallocate(ptr) 释放内存 void

这样,使用者一看就知道该怎么写自己的 Policy。

1.5 多个 Policy 的组合

实际项目中,一个类往往需要多个 Policy。比如一个线程安全的日志系统,可能需要:

  • 输出策略: 写文件、写网络、写控制台
  • 格式化策略: 纯文本、JSON、XML
  • 过滤策略: 按级别过滤、按模块过滤

用 Policy-Based 设计,可以这样组合:

template <typename OutputPolicy, typename FormatPolicy, typename FilterPolicy>
class Logger {
public:
    void log(const std::string& msg, int level) {
        if (!FilterPolicy::shouldLog(level)) return;
        auto formatted = FormatPolicy::format(msg, level);
        OutputPolicy::write(formatted);
    }
};

// 定义具体策略
struct FileOutput { static void write(const std::string& s) { /* 写文件 */ } };
struct JsonFormat { static std::string format(const std::string& msg, int level) { /* 转 JSON */ } };
struct LevelFilter { static bool shouldLog(int level) { return level >= WARN; } };

// 组合使用
using FileJsonLogger = Logger<FileOutput, JsonFormat, LevelFilter>;

注意: Policy 的数量不宜过多。我见过有人把 7、8 个 Policy 塞进一个模板类,结果代码可读性极差。一般来说,3-5 个 Policy 是比较合理的范围。

1.6 Policy 与默认参数

为了让使用者更方便,可以给 Policy 提供默认值:

template <
    typename OutputPolicy = FileOutput,
    typename FormatPolicy = PlainTextFormat,
    typename FilterPolicy = AllPassFilter
>
class Logger { /* ... */ };

// 使用者可以只指定需要的 Policy
using SimpleLogger = Logger<>;  // 全部默认
using JsonLogger = Logger<FileOutput, JsonFormat>;  // 只改格式化策略

这样既保留了灵活性,又降低了使用门槛。

1.7 我踩过的坑

我曾经在一个项目中,把 Policy 的接口设计得过于灵活——允许 Policy 返回不同的类型。结果导致模板实例化时出现各种奇怪的编译错误,调试了整整两天。

后来我总结了一条原则:Policy 的接口签名应该尽量统一。如果确实需要返回不同类型,可以用 using 类型别名来约定:

struct PolicyA {
    using result_type = int;
    static result_type compute() { return 42; }
};

struct PolicyB {
    using result_type = double;
    static result_type compute() { return 3.14; }
};

这样宿主类可以通过 typename Policy::result_type 来获取返回类型,既灵活又可控。

1.8 知识体系总览

下面这张图展示了 Policy-Based 设计的核心结构:

Policy-Based 设计核心结构 宿主类 (Host Class) template<typename Policy1, typename Policy2> 调用 调用 Policy1 分配策略 allocate() deallocate() Policy2 锁定策略 lock() unlock() MallocAlloc PoolAlloc SpinLock MutexLock 组合示例 using MyClass = HostClass<MallocAlloc, SpinLock>;

从图中可以看到,宿主类通过模板参数接收 Policy,Policy 再绑定到具体的实现。使用者只需要组合不同的 Policy,就能得到不同的行为。

1.9 什么时候用 Policy-Based?

不是所有场景都适合用 Policy-Based 设计。我个人的经验是:

  • 适合: 类的行为有多个独立维度,每个维度有 2 种以上选择
  • 适合: 需要在编译期确定策略,运行时不需要切换
  • 不适合: 策略需要在运行时动态切换(用策略模式更合适)
  • 不适合: 策略数量极少,且不会扩展(用简单继承就够了)

一句话总结: Policy-Based 设计是编译期的策略模式。它用模板参数代替虚函数,用编译期绑定代替运行时多态。代价是代码体积可能增大,但换来的是零运行时开销和极致的灵活性。

好了,这一章就到这里。Policy-Based 设计是 C++ 模板元编程中非常实用的技术,希望你能在实际项目中用起来。


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