11、设计模式与C++:单例模式(线程安全)、工厂模式、观察者模式、CRTP奇异递归模板模式

设计模式这东西,说白了就是前人踩坑踩出来的「最佳实践」。我刚开始写C++那会儿,觉得模式都是花架子,直到接手一个百万行级别的遗留系统……嗯,没有模式,代码就像一团乱麻。今天咱们聊聊四个在C++里特别实用的模式:单例、工厂、观察者,还有那个名字很唬人的CRTP。

一、单例模式:全局唯一,但别滥用

单例模式保证一个类只有一个实例,并提供一个全局访问点。听起来简单,但线程安全是个大坑。

1.1 饿汉式 vs 懒汉式

饿汉式:程序启动时就创建实例。简单,但如果你这个单例很重,启动慢不说,还可能根本用不上。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11 保证线程安全
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

懒汉式:第一次使用时才创建。需要加锁,否则多线程下会创建多个实例。

注意:我见过有人用双重检查锁定(DCLP),但在C++03下是有问题的。C++11之后,局部静态变量的初始化是线程安全的,所以上面的写法就够了。别自己折腾锁,容易出事。

1.2 我踩过的坑

我曾经在一个网络库里用了单例管理连接池。结果程序退出时,多个单例的析构顺序不可控,导致崩溃。后来我改用 std::shared_ptr 管理生命周期,或者干脆不用单例,改用依赖注入。

建议:单例适合日志、配置、线程池这类全局资源。但别把业务逻辑塞进单例,否则测试起来很痛苦。

二、工厂模式:把创建对象这件事抽象出来

工厂模式的核心思想是:不直接 new 对象,而是通过一个工厂方法来创建。这样客户端代码就不依赖具体类了。

2.1 简单工厂

enum class ProductType { A, B };

class Product { /* ... */ };
class ProductA : public Product { /* ... */ };
class ProductB : public Product { /* ... */ };

class SimpleFactory {
public:
    static std::unique_ptr<Product> create(ProductType type) {
        switch (type) {
            case ProductType::A: return std::make_unique<ProductA>();
            case ProductType::B: return std::make_unique<ProductB>();
            default: return nullptr;
        }
    }
};

简单工厂的问题在于:每加一种产品,就要改 switch 语句。违反了开闭原则。

2.2 工厂方法模式

把工厂也抽象成接口,每个具体产品对应一个具体工厂。这样加新产品时,只需要新增一个工厂类,不用改已有代码。

class Factory {
public:
    virtual std::unique_ptr<Product> create() = 0;
};

class FactoryA : public Factory {
public:
    std::unique_ptr<Product> create() override {
        return std::make_unique<ProductA>();
    }
};

我个人习惯用工厂方法模式来处理「不同平台下的相同接口」。比如Windows和Linux下的文件操作,各写一个工厂,客户端代码完全不用关心平台差异。

三、观察者模式:一对多的依赖关系

观察者模式定义了一种一对多的依赖关系:当一个对象状态改变时,所有依赖它的对象都会收到通知。说白了,就是「发布-订阅」。

3.1 经典实现

class Observer {
public:
    virtual void update(const std::string& message) = 0;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> observers_;
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);
    }
    void notify(const std::string& msg) {
        for (auto& wobs : observers_) {
            if (auto obs = wobs.lock()) {
                obs->update(msg);
            }
        }
    }
};

关键点:std::weak_ptr 避免循环引用。观察者如果持有主题的指针,主题又持有观察者的 shared_ptr,那就泄漏了。我见过线上服务因为这个内存只增不减,排查了好久。

3.2 现代C++的替代方案

其实现在很多场景可以用 std::function 和信号槽库(比如 Boost.Signals2)替代。但如果你不想引入第三方库,自己实现一个轻量级的观察者模式也不难。

你想想看,观察者模式最典型的应用就是 GUI 事件处理。按钮点击、鼠标移动,都是观察者模式。我写过一个游戏引擎,用观察者模式处理碰撞事件,效果很好。

四、CRTP:奇异递归模板模式

CRTP 这个名字很唬人,其实核心就是:派生类把自己作为模板参数传给基类。

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

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

这有什么用?说白了,就是实现「静态多态」。虚函数有运行时开销(虚表、虚指针),CRTP 在编译期就确定了调用关系,性能更好。

4.1 我常用的场景

  • 对象计数:统计某个类有多少个实例。
  • 单例模式:用 CRTP 实现一个通用的单例基类。
  • 策略模式:编译期选择算法,避免虚函数开销。
template <typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }
protected:
    Singleton() = default;
    ~Singleton() = default;
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

class Logger : public Singleton<Logger> {
    friend class Singleton<Logger>;
    // 实际日志实现
};

注意:CRTP 虽然性能好,但代码可读性差。团队里如果有人不熟悉模板,可能会看不懂。我建议只在性能敏感的核心模块使用,业务代码还是用虚函数更清晰。

五、知识体系总览

下面这张图总结了四种模式的核心关系和应用场景:

C++ 设计模式核心知识体系 单例模式 工厂模式 观察者模式 CRTP 日志、配置、连接池 线程安全需注意 对象创建解耦 工厂方法 vs 抽象工厂 事件驱动、GUI weak_ptr避免循环引用 静态多态、对象计数 编译期绑定,性能好 选择建议 • 需要全局唯一实例 → 单例模式 • 需要解耦对象创建 → 工厂模式 • 需要一对多通知 → 观察者模式 • 需要零开销抽象 → CRTP

六、总结

设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。记住:模式是工具,不是目的。

  • 单例:全局唯一,但小心生命周期和线程安全。
  • 工厂:解耦创建,适合复杂对象构建。
  • 观察者:事件通知,注意内存管理。
  • CRTP:编译期多态,性能敏感场景的利器。

嗯,今天就聊到这儿。这些模式在实际项目中几乎天天用,你写代码时多想想「这里能不能用模式重构一下」,慢慢就有感觉了。


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