【C++】设计一个单例基类,用户使用基类提供 getInstance() 来获取派生类实例化的单例对象

张开发
2026/5/5 12:12:27 15 分钟阅读
【C++】设计一个单例基类,用户使用基类提供 getInstance() 来获取派生类实例化的单例对象
这是一个非常高级且实用的 C 设计模式需求。要实现一个基类既能作为单例的通用接口又能自动管理派生类的实例化我们需要结合CRTP和工厂模式。这种设计通常被称为“可注册的单例基类”。 核心设计思路基类 (SingletonBase)提供统一的getInstance()接口。维护一个工厂映射表std::map用于存储“类型ID”到“创建函数”的映射。提供registerType方法允许派生类在程序启动时注册自己。派生类 (Derived)继承自基类。在静态初始化阶段自动向基类注册自己的“构造工厂”。获取实例调用SingletonBase::getInstanceT()时基类查找映射表动态创建并返回对应的派生类单例。 代码实现#includeiostream#includememory#includemap#includestring#includefunctional#includevector// 1. 基类提供统一的单例管理接口classSingletonBase{public:// 禁止拷贝SingletonBase(constSingletonBase)delete;SingletonBaseoperator(constSingletonBase)delete;// 虚析构函数确保多态删除时安全virtual~SingletonBase()default;// 业务接口virtualvoiddoWork()0;// --- 核心魔法泛型获取实例 ---templatetypenameTstaticTgetInstance(){// 获取当前类型的唯一静态实例// C11 保证 static 局部变量的线程安全初始化staticT instance;returninstance;}// --- 可选通过字符串/ID 获取基类指针 (工厂模式) ---// 这在不知道具体类型只知道“名字”时很有用usingCreatorFuncstd::functionSingletonBase*();staticstd::mapstd::string,CreatorFuncgetFactoryMap(){staticstd::mapstd::string,CreatorFuncfactoryMap;returnfactoryMap;}// 注册类型到工厂staticvoidregisterType(conststd::stringname,CreatorFunc creator){getFactoryMap()[name]creator;}// 通过名字创建对象 (注意这里返回的是裸指针或智能指针因为类型在运行时决定)staticSingletonBase*createByName(conststd::stringname){autoitgetFactoryMap().find(name);if(it!getFactoryMap().end()){returnit-second();}returnnullptr;}protected:SingletonBase(){}};// // 派生类 A// classConcreteSingletonA:publicSingletonBase{public:// 2. 派生类必须禁止拷贝ConcreteSingletonA(constConcreteSingletonA)delete;ConcreteSingletonAoperator(constConcreteSingletonA)delete;voiddoWork()override{std::coutConcreteSingletonA is working.std::endl;}voidspecificMethodA(){std::coutMethod specific to A.std::endl;}protected:// 3. 构造函数 protected防止外部 new只允许单例模式或基类管理ConcreteSingletonA(){std::coutA constructed.std::endl;}~ConcreteSingletonA(){}private:// 4. 注册器类 (利用静态成员在程序启动时自动执行)structRegistrar{Registrar(){// 将 TypeA 这个名字映射到 创建 ConcreteSingletonA 的函数// 注意这里我们注册的是创建单例的工厂而不是直接 new 对象// 为了演示工厂模式这里演示如何注册“创建单例”的逻辑registerType(TypeA,[](){// 这里返回的是单例的引用地址但在工厂接口中我们通常返回指针// 为了保持单例特性工厂内部也调用 getInstancereturngetInstanceConcreteSingletonA();});std::coutRegistered TypeA.std::endl;}};// 静态成员变量触发 Registrar 的构造staticRegistrar registrar;};// 初始化静态成员触发注册ConcreteSingletonA::Registrar ConcreteSingletonA::registrar;// // 派生类 B// classConcreteSingletonB:publicSingletonBase{public:ConcreteSingletonB(constConcreteSingletonB)delete;ConcreteSingletonBoperator(constConcreteSingletonB)delete;voiddoWork()override{std::coutConcreteSingletonB is working.std::endl;}protected:ConcreteSingletonB(){std::coutB constructed.std::endl;}~ConcreteSingletonB(){}private:structRegistrar{Registrar(){registerType(TypeB,[](){returngetInstanceConcreteSingletonB();});std::coutRegistered TypeB.std::endl;}};staticRegistrar registrar;};ConcreteSingletonB::Registrar ConcreteSingletonB::registrar;// // 使用演示// intmain(){std::cout--- 1. 直接使用泛型接口获取单例 (推荐) ---std::endl;// 方式一如果你知道具体类型直接使用模板接口最安全、最高效ConcreteSingletonAa1SingletonBase::getInstanceConcreteSingletonA();a1.doWork();ConcreteSingletonBb1SingletonBase::getInstanceConcreteSingletonB();b1.doWork();std::cout\n--- 2. 验证单例唯一性 ---std::endl;ConcreteSingletonAa2SingletonBase::getInstanceConcreteSingletonA();std::couta1 address: a1std::endl;std::couta2 address: a2std::endl;// 地址应该相同std::cout\n--- 3. 使用工厂模式通过字符串获取 (多态) ---std::endl;// 方式二如果你只知道名字比如配置文件读取使用工厂模式std::string configTypeTypeA;SingletonBase*ptrSingletonBase::createByName(configType);if(ptr){ptr-doWork();// 多态调用}// 获取 TypeBSingletonBase*ptrBSingletonBase::createByName(TypeB);if(ptrB){ptrB-doWork();// 验证工厂返回的也是单例 (地址应该和上面的 b1 相同)std::coutFactory B address: ptrBstd::endl;std::coutDirect B address: b1std::endl;}return0;} 关键点解析1. 基类的template typename T static T getInstance()这是最核心的部分。我们将getInstance放在基类中但是它是模板函数。当调用SingletonBase::getInstanceConcreteSingletonA()时编译器会实例化这个函数其中的static T instance实际上变成了static ConcreteSingletonA instance。优点你不需要在每个派生类里重复写getInstance代码。基类自动为每个类型T生成一个单例存储位置。2. 自动注册机制 (Registrar)为了让基类知道有哪些派生类为了支持createByName我们需要在程序启动时注册它们。利用 C 全局/静态变量的初始化特性我们在派生类内部定义一个struct Registrar和一个静态成员registrar。当程序加载时registrar被构造它的构造函数会自动调用基类的registerType将“类型名”和“创建该单例的 Lambda 函数”存入基类的 Map 中。3. 线程安全代码中使用了Meyers’ Singleton(static T instance)。C11 标准保证静态局部变量的初始化是线程安全的即“魔法静态变量”。这意味着即使多线程同时调用getInstance也只会创建一个实例。4. 工厂模式与单例的结合在registerType中我们注册的 Lambda 表达式是[]() { return getInstanceT(); }。这非常巧妙即使通过工厂接口通常用于创建新对象来获取实例我们内部依然强制返回了单例对象的地址。这保证了无论通过哪种方式模板直接调用 或 字符串工厂调用拿到的都是同一个对象。 总结这种设计模式非常适合插件系统或组件化架构扩展性强新增一个单例类只需继承SingletonBase并复制粘贴Registrar代码即可无需修改基类。统一管理所有单例的生命周期管理逻辑都集中在基类的模板方法中。多态支持可以通过基类指针操作不同的单例实现。

更多文章