首页 / 资讯中心 / 文章详情

C++通用动态抽象工厂的实现详解

C++通用动态抽象工厂的实现详解 ★ FEATURED ARTICLE
前言抽象工厂abstract factory模式要解决的问题是调用方只依赖抽象产品接口具体创建哪一个产品由配置或运行期信息决定。经典实现是写一个工厂类里面用switch或if-else根据字符串、枚举创建不同子类。这种写法能用但每增加一个产品就要修改工厂违反开闭原则也让工厂成为整个项目里最常冲突的文件。「动态」和「通用」两个词指向同一个改进方向把「类型到创建函数」的映射从硬编码的switch搬到一张运行期注册表里并让每个具体产品在自己的源文件中完成注册。这样新增产品只需要新增文件工厂本身不用改。模板负责消除重复的注册代码std::function负责擦除具体类型。常见的误解有两个。第一把抽象工厂和工厂方法混为一谈工厂方法通常只创建一个产品抽象工厂强调「一族相关产品」但二者在 C 里都可以用注册表实现不必拘泥于 UML 名称。第二认为「自注册」一定安全全局对象的初始化顺序、静态库的链接裁剪、运行期多线程注册都会让看起来能用的注册表在真实工程里失效。本文按 C17 实现一个可用的版本并逐条说明这些边界。一、从 switch 工厂到注册表先看硬编码版本的问题// 适用于 C17std::unique_ptrProduct create(const std::string key) {if (key circle) return std::make_uniqueCircle();if (key square) return std::make_uniqueSquare();if (key triangle) return std::make_uniqueTriangle();return nullptr;}每加一个产品这个函数就要改一次还要重新编译所有依赖它的翻译单元。更麻烦的是工厂所在的库必须知道所有具体类导致「底层依赖上层」模块无法单独复用。注册表版本把依赖方向倒过来工厂只认识抽象类Product和「创建函数」这一概念具体类在各自文件里把创建函数注册进来。方案新增产品要改哪里依赖方向运行期可变switch工厂工厂函数工厂依赖所有具体类否注册表工厂新增一个源文件具体类依赖工厂是二、用std::function擦除创建函数的类型注册表需要存放「能创建一个Product的东西」。不同类型的构造函数参数不同不能直接用函数指针统一。std::functionstd::unique_ptrProduct()可以接纳任意可调用对象普通函数、lambda、std::bind表达式只要签名匹配。// 适用于 C17using Creator std::functionstd::unique_ptrProduct();std::mapstd::string, Creator creators_;这里选择返回std::unique_ptrProduct而不是裸指针是为了把所有权写进类型。调用方拿到指针就明确知道自己负责释放工厂也不必记忆谁创建了对象。若返回裸指针调用方很容易忘记delete或者误以为工厂持有所有权而重复释放。std::map的键用std::string查找是O(log n)。产品种类通常只有几十个这个开销可以接受如果注册表很大且查找频繁可以换成std::unordered_map平均O(1)代价是遍历顺序不确定——如果keys()需要稳定输出仍应使用有序容器或在返回前排序。三、自注册与静态初始化顺序最直观的写法是定义一个全局注册对象构造时把创建函数塞进工厂// 适用于 C17RegistrarCircle g_circleReg(circle);但工厂本身如果也是全局对象就遇到了「静态初始化顺序问题static initialization order fiasco」不同翻译单元里的全局对象初始化顺序未定义注册对象可能先于工厂构造访问到未初始化的工厂。标准只保证同一翻译单元内按定义顺序初始化跨文件不保证。解决办法是把工厂放进函数内的局部静态变量// 适用于 C17static ProductFactory instance() {static ProductFactory factory;return factory;}C11 起局部静态变量的初始化是线程安全的标准称为「magic statics」首次调用时构造之后直接返回。这样无论哪个翻译单元的注册对象先构造调用instance()都会确保工厂已经就绪。第二个坑是静态库裁剪。如果具体产品的注册对象只存在于一个静态库.a/.lib里而主程序没有引用该文件中的任何符号链接器可能整个目标文件都不拉进来注册代码根本不会执行。表现为「编译链接都成功运行时却说不认识这个产品」。常见对策是把产品注册放进显式引用的对象库、使用链接器的 whole-archive 选项或者干脆在main里显式调用一次注册函数。具体选项随工具链而异应以各自链接器文档为准。四、完整示例与参数化创建// 适用于 C17#include functional#include iostream#include map#include memory#include stdexcept#include string#include vector// ---------- 抽象产品 ----------class Product {public:virtual ~Product() default; // 多态基类必须虚析构virtual std::string name() const 0;virtual std::unique_ptrProduct clone() const 0;};// ---------- 具体产品 ----------class Circle : public Product {public:std::string name() const override { return Circle; }std::unique_ptrProduct clone() const override {return std::make_uniqueCircle(*this);}};class Square : public Product {public:std::string name() const override { return Square; }std::unique_ptrProduct clone() const override {return std::make_uniqueSquare(*this);}};// ---------- 注册表工厂 ----------class ProductFactory {public:using Creator std::functionstd::unique_ptrProduct();static ProductFactory instance() {static ProductFactory factory;return factory;}bool registerCreator(const std::string key, Creator creator) {return creators_.emplace(key, std::move(creator)).second;}std::unique_ptrProduct create(const std::string key) const {auto it creators_.find(key);if (it creators_.end()) {throw std::runtime_error(unknown product: key);}return it-second();}std::vectorstd::string keys() const {std::vectorstd::string result;result.reserve(creators_.size());for (const auto kv : creators_) result.push_back(kv.first);return result;}private:ProductFactory() default;std::mapstd::string, Creator creators_;};// ---------- 自注册辅助类 ----------template typename Tclass Registrar {public:explicit Registrar(const std::string key) {ProductFactory::instance().registerCreator(key, [] {return std::unique_ptrProduct(std::make_uniqueT());});}};// ---------- 在具体产品所在的翻译单元里注册 ----------namespace {RegistrarCircle g_circleReg(circle);RegistrarSquare g_squareReg(square);}int main() {ProductFactory factory ProductFactory::instance();for (const std::string key : factory.keys()) {std::unique_ptrProduct p factory.create(key);std::cout key - p-name() \n;std::unique_ptrProduct copy p-clone();std::cout clone: copy-name() \n;}try {factory.create(triangle);} catch (const std::runtime_error e) {std::cout e.what() \n;}return 0;}几个细节值得逐行核对。Registrar的构造函数只做一件事以键注册一个 lambda。lambda 里用std::make_uniqueT()创建对象再交给std::unique_ptrProduct的构造函数unique_ptrDerived到unique_ptrBase的转换是隐式的前提是Base有虚析构。ProductFactory的构造函数是私有的外部只能通过instance()取得单例避免出现多个注册表。registerCreator用emplace并返回bool重复键不会被静默覆盖调用方可以据此发现命名冲突。clone()需要具体类可拷贝。如果某个产品持有不可拷贝的资源比如文件句柄就不要把clone放进抽象接口改用其他复制策略否则每个子类都被迫实现一个语义可疑的拷贝构造。参数化创建与线程安全真实业务里产品构造函数往往需要参数。把Creator改成std::functionstd::unique_ptrProduct(const Config)create多接一个const Config注册端用带捕获的 lambda 或直接传std::make_uniqueT, const Config形式的包装即可。注意捕获引用时要确保注册对象的生命周期长于调用静态注册通常没问题局部注册则要小心悬垂引用。静态初始化阶段的注册发生在main之前通常是单线程的但如果程序在运行期动态注册例如插件加载std::map的读写就存在数据竞争数据竞争在 C 里是未定义行为。此时要给registerCreator和create加同一把std::mutex读多写少时也可以考虑读写锁但不要因为「注册很少发生」就省略同步。常见坑点1. 工厂里保留switch❌ 每新增一个产品就修改create函数重新编译整个模块。 ✅ 用注册表新增产品只新增源文件和一行注册。2. 返回裸指针❌Product* create(const std::string key);所有权含糊调用方容易泄漏。 ✅ 返回std::unique_ptrProduct把所有权写进类型。3. 工厂是全局对象❌ProductFactory g_factory;与注册对象在不同文件初始化顺序未定义。 ✅ 用函数内局部静态static ProductFactory factory;首次调用时构造。4. 注册键重复被静默覆盖❌creators_[key] std::move(creator);后注册的悄悄替换先注册的。 ✅ 用emplace并检查返回值重复键时报错或打日志。5. 静态库中的注册代码被链接器丢弃❌ 把产品实现放进静态库主程序没有引用其符号运行时提示未知产品。 ✅ 使用对象库、whole-archive 链接选项或在main中显式调用注册函数。6. 抽象基类析构不是虚函数❌class Product { public: ~Product() default; };通过基类指针删除派生对象是 UB。 ✅ 写virtual ~Product() default;。7. 运行期注册不加锁❌ 插件线程调用registerCreator主线程同时createstd::map数据竞争。 ✅ 读写共用一把std::mutex或用std::shared_mutex保护。8. 创建函数为空❌std::function默认构造后直接return it-second();抛std::bad_function_call。 ✅ 注册时校验可调用性或捕获异常并给出明确错误信息。总结组成作用关键点抽象产品统一接口虚析构不可省具体产品实现细节各自翻译单元注册Creator类型擦除std::function返回unique_ptr注册表键到创建函数重复键检测Registrar自注册静态对象构造即注册instance()单例入口局部静态规避初始化顺序通用动态抽象工厂的本质是把「创建什么」从编译期分支变成运行期查表。模板让注册代码只写一次std::function抹平构造差异局部静态单例解决初始化顺序。真正需要工程经验的是链接裁剪与并发注册这两处它们不会在单元测试里暴露却会在集成阶段让注册表「莫名其妙少了几项」。
阅读完成 · 觉得有帮助?
咨询建站