基于 C++17 与 RTTR 的注册式对象工厂:配置驱动的动态对象创建

在工程开发里一个非常典型的需求:配置文件(XML、JSON、ini等)仅记录一个类名字符串,程序启动后读取这个名字,自动实例化出对应的C++对象。如果类型固定、数量很少,我们可以直接new或者一堆if-else / switch硬编码分支;但当业务持续迭代、对象类型源源不断新增,每次加类都要修改加载器的判断逻辑、重新编译核心加载代码,这套方案就会变得臃肿、脆弱、违背开闭原则。**注册式工厂(Registry Factory)**就是用来解决这个问题的经典方案,它的核心思想是提前把【类型名称】和【对象创建函数】绑定,存入一张全局注册表,运行时只需要根据配置读出的类名,查表拿到创建入口,统一调用创建逻辑。

我们可以循序渐进理解这套机制:从最简单的直接构造、分支判断,再引入模板简化重复代码,接着结合反射库RTTR实现通用注册,最后对接配置文件,形成一套稳定、可扩展的加载流程,下面全部基于C++17标准库演示。

目录

一、起点:硬编码直接构造与if-else分支(反例)

二、第一步改进:把创建行为抽象成可存储的函数

三、模板封装:自动生成创建函数(纯标准库工厂)

四、引入RTTR:基于运行时反射的通用对象创建

[五、串联完整链路:XML配置 → 名称查找 → 工厂/反射创建](#五、串联完整链路:XML配置 → 名称查找 → 工厂/反射创建)

六、设计思想总结


一、起点:硬编码直接构造与if-else分支(反例)

假设我们有两个基础组件类CameraSensor、RadarSensor,都继承同一个基类BaseSensor,加载器读取配置字符串后创建对象。

复制代码
#include <string>
#include <memory>

struct BaseSensor {
    virtual ~BaseSensor() = default;
    virtual void run() = 0;
};

struct CameraSensor : BaseSensor { void run() override; };
struct RadarSensor : BaseSensor { void run() override; };

// 最朴素写法:分支判断
std::unique_ptr<BaseSensor> create_by_name(const std::string& cls_name)
{
    if (cls_name == "CameraSensor") {
        return std::make_unique<CameraSensor>();
    } else if (cls_name == "RadarSensor") {
        return std::make_unique<RadarSensor>();
    }
    return nullptr;
}

这套代码逻辑直白,但缺陷非常明显:每新增一个Sensor子类,必须修改这个if-else列表。加载器代码持续膨胀,类型和创建逻辑耦合在一起,不符合"对扩展开放、对修改关闭"。当类型上百个、分散在不同模块,维护成本会急剧上升,这就是我们要改造的起点。

二、第一步改进:把创建行为抽象成可存储的函数

C++17的std::function可以保存无状态的创建回调。我们把"创建对象"这件事封装成一个函数签名,注册表本质就是一个std::unordered_map<std::string, CreatorFunc>,key是配置里的类名字符串,value是创建函数。

复制代码
#include <functional>
#include <unordered_map>

using CreatorFunc = std::unique_ptr<BaseSensor>();
std::unordered_map<std::string, std::function<CreatorFunc>> g_creator_registry;

// 注册接口:名字 + 创建函数
void register_class(const std::string& name, std::function<CreatorFunc> fn)
{
    g_creator_registry[name] = std::move(fn);
}

// 统一加载入口,不再写if-else
std::unique_ptr<BaseSensor> factory_create(const std::string& cls_name)
{
    auto it = g_creator_registry.find(cls_name);
    if (it == g_creator_registry.end()) return nullptr;
    return it->second();
}

现在加载器factory_create逻辑永久固定,不再随类型增加而改动。新增类的时候,只需要调用register_class把名字和构造函数注册进去。

复制代码
// 注册示例
register_class("CameraSensor", [](){ return std::make_unique<CameraSensor>(); });
register_class("RadarSensor", [](){ return std::make_unique<RadarSensor>(); });

但这里还有冗余:每注册一个类,都要手写lambda包装make_unique,重复样板代码多,接下来用模板消除重复。

三、模板封装:自动生成创建函数(纯标准库工厂)

利用C++17类模板,自动生成对应类型的创建闭包,不用手写lambda:

复制代码
template<class T>
std::unique_ptr<BaseSensor> type_creator()
{
    return std::make_unique<T>();
}

// 简化注册调用
register_class("CameraSensor", type_creator<CameraSensor>);
register_class("RadarSensor", type_creator<RadarSensor>);

更进一步,封装一个注册宏(工程中很常见),一行完成注册:

复制代码
#define REGISTER_CLASS(Cls) register_class(#Cls, type_creator<Cls>)

// 使用:REGISTER_CLASS(CameraSensor);

到这里,我们已经实现了纯C++17标准库的注册式工厂 。 它的边界是:注册动作必须在程序启动阶段显式调用;类型名是字符串硬绑定;工厂是业务自己手写的、只适配BaseSensor这一套继承树。如果项目里有多套不同基类的对象体系,就要写多套工厂、多张注册表,通用性不足。这就是反射框架RTTR登场的场景。

四、引入RTTR:基于运行时反射的通用对象创建

RTTR是一个轻量C++反射库,它的能力是:在运行期,通过类型名字符串拿到类型元信息、调用构造函数、遍历成员,不需要我们手写每一套工厂的创建回调。

它和上面手写工厂不是替代关系:手写工厂是【注册创建函数】,RTTR是【注册类型元信息,从元信息直接构造实例】,本质都属于名称到类型的映射,只是映射载体从自定义CreatorFunc变成了反射类型对象rttr::type。

RTTR基础注册与对象创建示例(C++17):

复制代码
#include <rttr/registration>
using namespace rttr;

RTTR_REGISTRATION
{
    registration::class_<CameraSensor>("CameraSensor")
        .constructor<>(); // 注册无参构造函数
    registration::class_<RadarSensor>("RadarSensor")
        .constructor<>();
}

// 通用加载函数:输入类名字符串,返回基类指针
std::unique_ptr<BaseSensor> rttr_create(const std::string& cls_name)
{
    auto t = type::get_by_name(cls_name);
    if (!t.is_valid()) return nullptr;
    variant obj = t.create();
    if (!obj.is_valid()) return nullptr;
    return obj.get_value<std::unique_ptr<BaseSensor>>();
}

对比前面手写的标准库工厂,RTTR的优势:

  1. 注册表由RTTR内部维护,不需要我们自己维护unordered_map;

  2. 一套反射注册机制,支持多套继承体系、不同基类;

  3. 除了创建对象,还附带运行时属性读写、方法调用,非常适合配置驱动的仿真框架;

  4. 类名直接在RTTR注册阶段绑定,和配置文件的类名一一对应。

在仿真框架场景下,这个流程非常契合:仿真引擎读取XML配置节点,节点属性包含class_name="CameraSensor",引擎把字符串传给rttr_create,得到仿真组件对象,加入仿真世界。后续新增仿真组件,只需要增加RTTR注册代码,核心仿真加载器一行不用改,完美满足"类型持续增加,加载流程不变"的需求。

五、串联完整链路:XML配置 → 名称查找 → 工厂/反射创建

完整数据流是:

  1. 程序启动阶段:所有组件模块执行注册(自定义工厂注册Creator 或 RTTR注册type+构造);

  2. 加载器读取XML配置,解析出<Sensor class="CameraSensor"/>中的类名字符串;

  3. 将字符串传入统一创建接口;

  4. 查表(自定义map / RTTR内部类型表)找到对应的创建入口;

  5. 调用构造,返回基类指针,交由仿真框架管理、执行仿真逻辑。

这里需要区分两种选型的取舍:

  • 纯C++17标准库注册工厂:无第三方依赖、编译快、可控性极强,适合性能敏感、不想引入反射库的模块;代价是需要自己维护注册表、创建函数,跨基类复用麻烦。

  • RTTR反射方案:代码更简洁,自带元信息,适合仿真、编辑器、配置驱动这类需要大量运行时类型信息的场景;代价是引入第三方库,有少量反射运行时开销。

六、设计思想总结

整个演进路径,是从"固定分支写死创建"到"注册式动态分发"的典型工程范式:

  1. 直接构造:最简单,完全不支持动态名称;

  2. if-else分支:可以按名字创建,但新增类型要修改加载核心,违反开闭原则;

  3. std::function + 注册表:把创建能力剥离出来,加载逻辑稳定,新增类只需要注册;

  4. 模板简化注册:消除重复样板代码,是标准库实现工厂的最优形态;

  5. RTTR反射:把【类型信息、构造函数、属性】统一注册,实现跨体系的通用创建,是仿真框架里非常流行的落地方式。

注册式工厂的本质,就是将"类型名称"和"对象构造能力"的绑定关系,从编译期硬编码的分支里抽离,放到一张运行时注册表中。配置文件只负责传递名字字符串,加载器只负责查表调用,类型的扩展对加载逻辑透明。这也是很多仿真引擎、插件框架、脚本绑定底层最基础的设计模式。

相关推荐
NineData44 分钟前
MySQL 迁移到 OceanBase 怎么做?NineData 全量迁移、CDC 实时同步与数据校验指南
数据库·mysql·oceanbase
Amos_Web1 小时前
Rspack 源码解析(二十九):CopyRspackPlugin 如何用 glob 把静态文件复制进产物
前端
zhangx1234_1 小时前
javaEE 多线程4
java·开发语言
铁皮饭盒1 小时前
刷到 FDE 岗位那一刻,我还以为前端又活了
前端·后端
standxy1 小时前
索引设计实战:pgvector HNSW × JSONB GIN × 复合索引
数据库·开发
guo_wen_qiang1 小时前
使用spring-data-mongodb操作mongo记录
数据库·mongodb
酷炫码神1 小时前
Day 9|Vue Router:把单页后台拆成能跳转的多页面
前端·javascript·vue.js
凤山老林1 小时前
授权模型只会RBAC?深入 ABAC 授权模型,实现权限判断做成策略引擎
大数据·数据库·算法
geovindu1 小时前
sql:SQL Views and Materialized Views using mysql 9.0
大数据·数据库·数据仓库·sql·mysql·数据库开发·数据库架构