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 虽然性能好,但代码可读性差。团队里如果有人不熟悉模板,可能会看不懂。我建议只在性能敏感的核心模块使用,业务代码还是用虚函数更清晰。
五、知识体系总览
下面这张图总结了四种模式的核心关系和应用场景:
六、总结
设计模式不是银弹。我见过有人为了用模式而用模式,把简单问题复杂化。记住:模式是工具,不是目的。
- 单例:全局唯一,但小心生命周期和线程安全。
- 工厂:解耦创建,适合复杂对象构建。
- 观察者:事件通知,注意内存管理。
- CRTP:编译期多态,性能敏感场景的利器。
嗯,今天就聊到这儿。这些模式在实际项目中几乎天天用,你写代码时多想想「这里能不能用模式重构一下」,慢慢就有感觉了。
公众号:蓝海资料掘金营,微信deep3321