模板设计模式总结:泛型工厂、泛型单例、泛型观察者模式

说实话,模板在C++里最让我着迷的地方,就是它能帮我们把设计模式玩出花来。你想想看,传统的设计模式大多依赖虚函数和继承,运行时多态虽然好用,但性能开销和代码膨胀有时候真让人头疼。而模板呢?它把多态搬到了编译期,零成本抽象,说白了就是“用编译时间换运行效率”。

我在好几个项目中都用模板重构过经典设计模式。今天咱们就聊聊三个最实用的:泛型工厂、泛型单例、泛型观察者。嗯,这些都是我踩过坑之后才真正理解透彻的。

泛型工厂:让对象创建不再“硬编码”

工厂模式大家都不陌生。传统写法里,每加一种新产品,就得改工厂类的switch-case或者if-else。我早期维护过一个插件系统,产品类型从十几个涨到上百个,那个工厂函数写得跟裹脚布似的。

用模板实现泛型工厂,核心思路是“注册表 + 类型映射”。我们把产品类型和对应的创建函数注册到一个map里,创建时通过类型ID查找并调用。

// 泛型工厂基类
template<typename Product, typename Key = std::string>
class GenericFactory {
public:
    using Creator = std::function<std::unique_ptr<Product>()>;

    bool registerCreator(const Key& key, Creator creator) {
        return creators_.emplace(key, std::move(creator)).second;
    }

    std::unique_ptr<Product> create(const Key& key) const {
        auto it = creators_.find(key);
        if (it != creators_.end()) {
            return it->second();
        }
        return nullptr;
    }

private:
    std::unordered_map<Key, Creator> creators_;
};

// 使用示例
class Shape { public: virtual void draw() = 0; };
class Circle : public Shape { void draw() override { /* ... */ } };
class Square : public Shape { void draw() override { /* ... */ } };

GenericFactory<Shape> shapeFactory;
shapeFactory.registerCreator("circle", []{ return std::make_unique<Circle>(); });
shapeFactory.registerCreator("square", []{ return std::make_unique<Square>(); });

auto shape = shapeFactory.create("circle");
我的习惯:注册动作通常放在一个静态初始化块里,或者用宏包装一下。这样新增产品时,只需要在对应源文件里加一行注册代码,工厂类本身纹丝不动。

这个模式的好处很明显:开闭原则贯彻到底,新增产品不需要改工厂代码。而且创建函数可以是lambda、函数指针、甚至是绑定了参数的std::bind,灵活得很。

泛型单例:线程安全与生命周期管理

单例模式,说白了就是“全局只有一个实例”。但传统单例有个老大难问题:线程安全。双检锁(DCLP)在C++里其实有坑,我记得以前在某个项目里就因为内存序的问题,导致多线程下拿到了半初始化的对象,排查了两天才找到原因。

C++11之后,标准保证了局部静态变量的初始化是线程安全的。所以泛型单例可以写得非常简洁:

template<typename T>
class Singleton {
public:
    static T& instance() {
        static T inst;
        return inst;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

protected:
    Singleton() = default;
    ~Singleton() = default;
};

// 使用
class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;
public:
    void log(const std::string& msg) { /* ... */ }
private:
    Logger() = default;
};

Logger::instance().log("hello");
我曾经踩过的坑:这个写法虽然线程安全,但有个隐含问题——析构顺序。如果两个单例互相依赖,一个析构时另一个还在用,那就崩了。我的建议是:单例之间尽量不要有依赖关系。实在避免不了,用std::shared_ptr管理生命周期,或者显式控制销毁顺序。

另外,泛型单例还可以扩展出“按线程单例”或“按上下文单例”。比如用thread_local存储实例,每个线程有自己的副本,这在无锁编程里很常见。

泛型观察者模式:事件驱动的编译期实现

观察者模式,就是“发布-订阅”。传统实现里,观察者基类有个虚函数update(),被观察者维护一个观察者列表。但虚函数调用有开销,而且类型擦除后,通知时往往要传一个void*或者基类指针,用起来不够安全。

用模板实现,我们可以把事件类型和回调函数类型都参数化。这样每个观察者只关心自己感兴趣的事件,类型安全,而且没有虚函数开销。

template<typename... EventTypes>
class Subject {
public:
    template<typename Event>
    using Callback = std::function<void(const Event&)>;

    template<typename Event>
    void subscribe(Callback<Event> cb) {
        auto& handlers = std::get<std::vector<Callback<Event>>>(callbacks_);
        handlers.push_back(std::move(cb));
    }

    template<typename Event>
    void notify(const Event& event) {
        const auto& handlers = std::get<std::vector<Callback<Event>>>(callbacks_);
        for (const auto& cb : handlers) {
            cb(event);
        }
    }

private:
    std::tuple<std::vector<Callback<EventTypes>>...> callbacks_;
};

// 使用
struct MouseEvent { int x, y; };
struct KeyEvent { int key; };

Subject<MouseEvent, KeyEvent> subject;

subject.subscribe<MouseEvent>([](const MouseEvent& e) {
    std::cout << "Mouse at " << e.x << ", " << e.y << "\n";
});

subject.notify(MouseEvent{100, 200});
核心优势:每个事件类型有独立的回调列表,互不干扰。编译器会为每种事件类型生成独立的代码,没有类型擦除,没有虚函数调用。性能上比传统观察者模式高出一截。

不过要注意,这个实现里回调是std::function,它本身有少量开销。如果对性能极度敏感,可以用函数指针或者模板回调。我在一个游戏引擎里用过类似方案,处理每帧上千个事件,完全没问题。

模板设计模式的最佳实践

说了这么多,总结几条我在实际项目里摸爬滚打出来的经验:

  • 编译期多态优先于运行时多态。 能用模板解决的问题,尽量别用虚函数。但也要权衡:模板会导致代码膨胀,如果类型数量很少(比如就两三种),虚函数可能更合适。
  • 接口要简洁,概念要清晰。 模板设计模式最容易犯的错就是过度泛化。我见过有人写了一个“万能工厂”,能创建任何类型、任何参数、任何生命周期……结果代码复杂到没人敢动。适可而止。
  • 错误信息要友好。 模板编译错误出了名的难读。我的习惯是在关键模板里加static_assert,给用户明确的提示。比如“类型必须继承自BaseClass”之类的。
  • 考虑二进制兼容性。 如果你在写库,模板会暴露所有实现细节,导致ABI不稳定。这种情况下,可以用“模板外壳 + 虚函数内核”的混合模式,外部用模板提供便利,内部用虚函数做稳定接口。
  • 测试要覆盖边界情况。 模板的实例化是隐式的,很容易漏掉某些类型组合。我建议用concept或SFINAE做约束,同时在单元测试里枚举所有预期的类型组合。
一个小技巧:写模板设计模式时,先写一个非模板的“具体版本”跑通逻辑,再逐步参数化。这样调试起来轻松很多,而且不容易把设计思路搞乱。

知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,泛型工厂、泛型单例、泛型观察者,本质上都是“用模板参数化变化点”,把设计模式的骨架固定下来,让使用者只关注业务逻辑。

模板设计模式知识体系 模板 + 设计模式 泛型工厂 注册表 + 类型映射 泛型单例 局部静态 + 线程安全 泛型观察者 类型安全事件分发 最佳实践:编译期优先 · 接口简洁 · 友好错误信息 · 考虑ABI · 充分测试 核心思想:用模板参数化变化点,零成本抽象

说实话,模板设计模式不是银弹。它适合那些“类型变化频繁但结构稳定”的场景。如果你的项目里类型几乎不变,或者运行时多态的开销可以接受,那传统写法反而更简单。但如果你追求极致性能、零成本抽象、以及编译期类型安全,那模板这条路值得深耕。

嗯,今天就聊到这儿。这些模式我在好几个项目里反复用过,每次都能发现新的优化点。模板这东西,越用越觉得它深不可测。


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