模板与设计模式:工厂模式、单例模式的模板化实现
设计模式这东西,说白了就是前人踩坑踩出来的经验总结。我早年刚接触C++那会儿,觉得设计模式就是金科玉律,照着抄准没错。后来写多了才发现——很多模式用模板一包装,代码量能砍掉一半,可维护性反而更高。
今天咱们就聊聊两个最常用的模式:工厂模式和单例模式。看看怎么用模板把它们写得既优雅又实用。
为什么要把设计模式模板化?
你想想看,传统设计模式最大的痛点是什么?重复劳动。每加一个新类型,就得写一堆相似的代码。我曾在项目中维护过一个产品工厂,里面十几个if-else分支,每次新增产品都得改工厂类——改完还要提心吊胆,生怕改漏了。
模板化之后呢?类型作为参数传进去,编译器帮你生成代码。新增产品?写个新类,模板自动适配。这才是C++该有的样子。
模板化单例模式
单例模式,大家都不陌生。全局只有一个实例,谁用谁拿。但传统写法有个问题——每个单例类都得写一遍GetInstance(),还得处理线程安全。
我个人的习惯是,用CRTP(奇异递归模板模式)做一个通用的单例基类:
template<typename T>
class Singleton {
public:
static T& GetInstance() {
static T instance;
return instance;
}
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;
};
这里有个坑,我必须要提醒你。很多人忘了加 friend class Singleton<Logger>,结果编译报错,然后一脸懵。为什么?因为 GetInstance() 里要构造 T 对象,但构造函数是私有的——基类不是友元的话,编译器不让调。
这种写法,我用了好几年。一个项目里十几个单例类,每个就三五行代码。清爽得很。
模板化工厂模式
工厂模式比单例复杂一些。传统工厂要维护一个“类型标识到创建函数”的映射。每新增一个产品,就得往映射表里注册。
用模板怎么搞?我见过两种主流做法,咱们都看看。
方案一:基于继承的模板工厂
template<typename Base, typename Key, typename... Args>
class Factory {
public:
using CreatorFunc = std::function<std::unique_ptr<Base>(Args...)>;
template<typename Derived>
static void Register(const Key& key) {
creators_[key] = [](Args... args) -> std::unique_ptr<Base> {
return std::make_unique<Derived>(std::forward<Args>(args)...);
};
}
static std::unique_ptr<Base> Create(const Key& key, Args... args) {
auto it = creators_.find(key);
if (it != creators_.end()) {
return it->second(std::forward<Args>(args)...);
}
return nullptr;
}
private:
static std::map<Key, CreatorFunc> creators_;
};
这个方案的好处是,注册和创建完全解耦。你可以在不同的编译单元里注册产品,甚至可以在运行时动态注册。我在一个插件系统里用过这个模式——每个插件DLL加载时自动注册自己,主程序完全不知道有哪些插件存在。
方案二:基于类型列表的编译期工厂
如果你不需要运行时动态注册,那编译期工厂更香。用类型列表和变参模板,让编译器帮你生成所有分支:
template<typename... Types>
class TypeList {};
template<typename Base, typename TypeList>
class CompileTimeFactory;
template<typename Base, typename... Types>
class CompileTimeFactory<Base, TypeList<Types...>> {
public:
template<typename... Args>
static std::unique_ptr<Base> Create(size_t index, Args... args) {
// 这里用if constexpr + 递归展开,或者用数组索引
static constexpr std::array<std::function<std::unique_ptr<Base>(Args...)>, sizeof...(Types)>
creators = {
[](Args... args) { return std::make_unique<Types>(std::forward<Args>(args)...); }...
};
if (index < creators.size()) {
return creators[index](std::forward<Args>(args)...);
}
return nullptr;
}
};
这种写法,说白了就是把运行时多态变成了编译期多态。没有虚函数开销,没有动态内存分配(除了产品对象本身)。性能敏感的场景下,我强烈推荐。
两种模式的组合使用
单例和工厂,其实经常一起出现。比如一个全局的工厂管理器,用单例包装一下:
using WidgetFactory = Singleton<Factory<Widget, std::string>>;
// 注册产品
WidgetFactory::GetInstance().Register<Button>("button");
WidgetFactory::GetInstance().Register<Slider>("slider");
// 创建产品
auto btn = WidgetFactory::GetInstance().Create("button");
你看,两行代码就搞定了全局工厂。不用到处传指针,也不用担心多实例问题。
知识体系总览
下面这张图,把咱们聊的内容串起来了:
避坑指南
最后,分享几个我踩过的坑:
- 单例的析构顺序:静态局部变量的析构顺序是构造的逆序。如果两个单例互相依赖,程序退出时可能崩溃。我建议单例之间不要有循环依赖。
- 工厂的线程安全:上面的运行时工厂,
Register和Create没有加锁。如果多线程同时注册和创建,会出问题。加个读写锁就能解决。 - 编译期工厂的索引:用
size_t index来选类型,索引必须和类型列表顺序一致。我习惯用一个枚举类来定义索引,避免硬编码数字。 - 模板代码膨胀:每个不同的模板参数都会生成一份代码。如果工厂的模板参数组合太多,二进制体积会暴涨。这时候可以考虑用
extern template显式实例化。
嗯,差不多就这些。模板和设计模式结合,写出来的代码既灵活又高效。但记住一点——不要为了用模板而用模板。如果你的项目里就两三个产品类型,手写工厂可能更简单。工具是拿来用的,不是拿来炫的。
公众号:蓝海资料掘金营,微信deep3321