44、STL与设计模式实战:工厂模式、单例模式、观察者模式在STL中的应用
说实话,很多C++开发者学了设计模式,也学了STL,但总觉得这两者是割裂的。我早年也有这个困惑——直到我在一个实时交易系统里,被逼着用STL容器去实现观察者模式,才恍然大悟:STL本身就是设计模式的最佳实践土壤。
这一章,咱们就聊聊三个经典模式——工厂模式、单例模式、观察者模式,看看它们怎么跟STL的容器、算法、智能指针结合。说白了,就是让你写出来的代码既有设计感,又有STL的效率和优雅。
工厂模式 + STL:用map管理对象生产
工厂模式的核心,是把对象的创建逻辑封装起来。但传统工厂写出来,往往是一堆if-else或者switch-case。我个人习惯用std::map或std::unordered_map来替代这些分支判断。
举个例子,假设我们要创建不同类型的日志处理器:
#include <iostream>
#include <memory>
#include <unordered_map>
#include <functional>
#include <string>
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& msg) = 0;
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[Console] " << msg << std::endl;
}
};
class FileLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "[File] " << msg << std::endl; // 实际应写入文件
}
};
// 工厂类:用unordered_map注册创建函数
class LoggerFactory {
public:
using Creator = std::function<std::unique_ptr<Logger>()>;
static LoggerFactory& instance() {
static LoggerFactory factory;
return factory;
}
void registerLogger(const std::string& type, Creator creator) {
creators_[type] = std::move(creator);
}
std::unique_ptr<Logger> create(const std::string& type) {
auto it = creators_.find(type);
if (it != creators_.end()) {
return it->second();
}
return nullptr;
}
private:
std::unordered_map<std::string, Creator> creators_;
LoggerFactory() = default;
};
// 注册产品
auto consoleReg = []() {
LoggerFactory::instance().registerLogger("console",
[](){ return std::make_unique<ConsoleLogger>(); });
return true;
}();
auto fileReg = []() {
LoggerFactory::instance().registerLogger("file",
[](){ return std::make_unique<FileLogger>(); });
return true;
}();
int main() {
auto logger = LoggerFactory::instance().create("console");
if (logger) logger->log("Hello from factory!");
return 0;
}
关键点:用std::function + std::unique_ptr,工厂不再需要硬编码if-else。新增一种日志类型,只需要注册一个lambda,开闭原则完美落地。
我在项目中遇到过一个问题:注册顺序依赖导致崩溃。后来我改用std::map(有序)来保证注册顺序可预测,调试起来舒服多了。
单例模式 + STL:线程安全的局部静态变量
单例模式在C++11之后变得极其简单——局部静态变量的初始化是线程安全的。但如果你需要管理一组单例对象,STL容器就派上用场了。
我个人喜欢用std::map<std::string, std::shared_ptr<T>>来实现一个「单例注册表」:
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
class SingletonRegistry {
public:
static SingletonRegistry& getInstance() {
static SingletonRegistry instance;
return instance;
}
template<typename T>
std::shared_ptr<T> get(const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = registry_.find(key);
if (it != registry_.end()) {
return std::static_pointer_cast<T>(it->second);
}
auto obj = std::make_shared<T>();
registry_[key] = obj;
return obj;
}
private:
std::map<std::string, std::shared_ptr<void>> registry_;
std::mutex mutex_;
SingletonRegistry() = default;
};
// 使用示例
class ConfigManager {
public:
void load() { std::cout << "Config loaded." << std::endl; }
};
int main() {
auto cfg = SingletonRegistry::getInstance().get<ConfigManager>("config");
cfg->load();
return 0;
}
注意:用std::shared_ptr<void>做类型擦除,取回时需要static_pointer_cast。我曾经因为类型转换错误导致运行时崩溃,所以建议在get模板里加typeid断言做安全检查。
观察者模式 + STL:用vector/list管理订阅者
观察者模式在GUI、事件系统里太常见了。STL的std::vector或std::list天然适合存储观察者列表。但有个坑——遍历时如果观察者被移除,迭代器会失效。
我建议用std::vector<std::weak_ptr<Observer>>,配合std::remove_if来清理失效的观察者:
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <string>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& event) = 0;
};
class Subject {
public:
void attach(std::shared_ptr<Observer> obs) {
observers_.push_back(obs);
}
void detach(const std::shared_ptr<Observer>& obs) {
// 使用erase-remove惯用法移除指定观察者
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[&obs](const std::weak_ptr<Observer>& wp) {
auto sp = wp.lock();
return !sp || sp == obs;
}),
observers_.end());
}
void notify(const std::string& event) {
// 先清理失效的weak_ptr,再通知
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](const std::weak_ptr<Observer>& wp) {
return wp.expired();
}),
observers_.end());
for (auto& wp : observers_) {
if (auto sp = wp.lock()) {
sp->update(event);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};
// 具体观察者
class ConcreteObserver : public Observer {
public:
explicit ConcreteObserver(const std::string& name) : name_(name) {}
void update(const std::string& event) override {
std::cout << name_ << " received: " << event << std::endl;
}
private:
std::string name_;
};
int main() {
Subject subject;
auto obs1 = std::make_shared<ConcreteObserver>("Observer1");
auto obs2 = std::make_shared<ConcreteObserver>("Observer2");
subject.attach(obs1);
subject.attach(obs2);
subject.notify("EventA");
subject.detach(obs1);
subject.notify("EventB");
return 0;
}
小技巧:用std::weak_ptr而不是裸指针,可以自动处理观察者被销毁的情况。配合std::remove_if,每次通知前清理一次,既安全又高效。
三种模式与STL的融合关系图
下面这张图展示了三种设计模式与STL核心组件的对应关系,方便你快速回顾:
避坑指南与个人经验
- 工厂模式:我曾经在注册lambda时捕获了局部变量,导致悬空引用。建议lambda里只捕获必要的智能指针,或者用
std::bind绑定参数。 - 单例模式:别在单例构造函数里调用其他单例——静态初始化顺序未定义。我吃过这个亏,后来改用
std::call_once+ 显式初始化。 - 观察者模式:遍历时如果观察者内部触发了detach,迭代器会失效。我的做法是:通知前先拷贝一份观察者列表的
shared_ptr副本,遍历副本,原列表加锁保护。
核心思想:STL不是设计模式的替代品,而是实现设计模式的「乐高积木」。你想想看,用std::map做工厂注册表,用std::weak_ptr做观察者生命周期管理,用std::remove_if做清理——这些组合拳,比手写链表、手写线程安全队列不知道高到哪里去了。
嗯,这一章就到这里。记住:设计模式是思想,STL是工具。把思想装进工具里,你的代码才能既优雅又健壮。