C++ 字符串动态创建对象 -- 工厂模式、自动注册、模板递归动态调用

在游戏服务器或游戏引擎框架中,经常会遇到这样一个问题:对象并不总是由业务代码直接 new 出来,而是可能来自配置、网络协议、脚本、GM 命令,甚至是跨线程消息。

例如登录服中可能有这样的代码:

cpp 复制代码
void LoginApp::InitApp()
{
    Account* pAccount = new Account();
    pThreadMgr->AddObjToThread(pAccount);
}

这段代码本身没有语法问题,但在框架设计上存在明显局限。Account 如果是一个由框架管理的 EntityComponent,它的创建、生命周期、线程归属、注册关系、消息绑定,都不应该散落在业务代码里。更理想的做法是:外部只描述"我要创建什么对象",真正创建动作交给框架完成

我们希望最终能写出类似这样的代码:

cpp 复制代码
Component* account = CreateComponent("Account");
Component* player  = CreateComponent("Player");
Component* scene   = CreateComponent("Scene");

如果对象需要构造参数,也希望支持:

cpp 复制代码
Component* player = CreateComponent("Player", 1001, std::string("ayanami"));

这就是本文要讨论的问题:在 C++ 中如何根据字符串动态创建类对象,并支持不同构造函数参数。


1. 为什么 C++ 不能天然通过字符串创建对象

在 Java、C# 这类语言中,反射系统比较完整,可以通过类名、类型元信息动态创建对象。C++ 的设计重点不同。C++ 有 RTTI,但 RTTI 主要用于 typeiddynamic_cast 等有限场景,它并不提供一个标准机制,让你可以写:

cpp 复制代码
Component* obj = CreateByClassName("Account");

然后自动生成:

cpp 复制代码
new Account();

也就是说,C++ 编译器不会在运行时帮你维护一张**"类名字符串 -> 构造函数"的映射表**。

所以如果我们想通过字符串创建类对象,就必须自己建立一套机制。这个机制通常由三部分组成:

  1. 工厂:负责保存创建函数 ,并根据字符串查找创建函数
  2. 注册:负责把某个类的名字和创建函数绑定起来
  3. 调用适配:负责把运行时参数转换成 C++ 能调用的构造函数参数(例如把协议中的参数转换成C++能调用的构造函数参数)

2. 直接 new 对象的问题

还是回到最初的例子:

cpp 复制代码
void LoginApp::InitApp()
{
    Account* pAccount = new Account();
    pThreadMgr->AddObjToThread(pAccount);
}

这个写法会带来几个工程问题:

第一,对象创建逻辑分散。今天在 LoginApp 里创建 Account,明天在 SceneApp 里创建 Scene,后天又在网络消息处理中创建 Player。一旦对象创建过程要增加初始化步骤,所有地方都要改。

第二,生命周期不统一。框架中的 EntityComponent 往往需要统一管理,可能要进入 EntitySystem,也可能要挂接到某个线程、某个消息队列、某个对象池。如果直接 new,很容易绕过框架生命周期管理。

第三,线程归属不清晰。Actor 模型通常要求对象只在所属线程内被访问。外部线程最好只投递"创建请求",真正的对象创建由目标线程中的 EntitySystem 完成。

第四,不利于配置化和协议化。假设网络协议中传来:

text 复制代码
class_name = "Account"

业务希望根据这个字符串创建 Account。如果代码里到处都是 new Account(),就很难支持这种动态创建。

因此,我们需要把对象创建改造成一种框架能力:

text 复制代码
配置 / 协议 / 脚本 / GM 命令
        ↓
className + params
        ↓
工厂查表
        ↓
调用创建函数
        ↓
EntitySystem 接管对象

3. 核心思想:字符串到创建函数的映射

通过字符串动态创建对象的核心并不复杂,本质上就是维护一张表:

text 复制代码
"Account" -> 创建 Account 对象的函数
"Player"  -> 创建 Player 对象的函数
"Scene"   -> 创建 Scene 对象的函数

在 C++ 中可以用 std::unordered_mapstd::function 表示:

cpp 复制代码
std::unordered_map<std::string, std::function<Component*()>> _map;

创建时查表:

cpp 复制代码
auto iter = _map.find(className);
if (iter != _map.end())
{
    return iter->second();
}

其中 iter->second() 是一个函数对象,它内部可能执行:

cpp 复制代码
return new Account();

这就是工厂模式的一种实现方式。

这里有一个关键点:字符串本身不会创建对象,字符串只是 key;真正创建对象的是注册到工厂里的函数。


4. 注册机制:告诉系统字符串对应哪个类

C++ 不会自动知道:

text 复制代码
"Account" 对应 class Account
"Player"  对应 class Player

所以必须显式注册:

cpp 复制代码
factory.Regist("Account", []() -> Component* {
    return new Account();
});

一个最简单的无参版本工厂可以这样写:

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

class Component
{
public:
    virtual ~Component() = default;
};

class ComponentFactory
{
public:
    using Creator = std::function<Component*()>;

    bool Regist(const std::string& className, Creator creator)
    {
        if (!creator)
        {
            return false;
        }

        return _map.emplace(className, creator).second;
    }

    Component* Create(const std::string& className)
    {
        auto iter = _map.find(className);
        if (iter == _map.end())
        {
            return nullptr;
        }

        return iter->second();
    }

private:
    std::unordered_map<std::string, Creator> _map;
};

Regist 的职责是把类名和创建函数放进 _map

Create 的职责是根据类名查找创建函数,然后调用它。

例如:

cpp 复制代码
class Account : public Component
{
};

ComponentFactory factory;

factory.Regist("Account", []() -> Component* {
    return new Account();
});

Component* account = factory.Create("Account");

这已经实现了最基础的"字符串创建对象"。

但是它只能处理无参构造。如果类的构造函数需要参数,就需要继续扩展。


5. 支持构造参数:ComponentFactory<Targs...>

实际业务类通常不一定是无参构造。例如:

cpp 复制代码
#include <iostream>
#include <string>

class Test1 : public Component
{
public:
    explicit Test1(const std::string& name)
    {
        std::cout << "create Test1: " << name << std::endl;
    }
};

class Test2 : public Component
{
public:
    explicit Test2(int id)
    {
        std::cout << "create Test2: " << id << std::endl;
    }
};

Test1 的创建函数签名类似:

cpp 复制代码
Component*(std::string)

Test2 的创建函数签名类似:

cpp 复制代码
Component*(int)

这两个函数签名不同,不能放进同一个 std::function<Component*()> 中。

因此可以把工厂本身做成可变参数模板,这里的类模板参数是构造函数的参数列表,也就是说同一个类模板实例化出来不同的实例所对应的参数列表不同:

cpp 复制代码
#include <functional>
#include <string>
#include <unordered_map>
#include <utility>

template<typename... Targs>
class ComponentFactory
{
public:
    using Creator = std::function<Component*(Targs&&...)>;

    static ComponentFactory* Instance()
    {
        static ComponentFactory instance;
        return &instance;
    }

    bool Regist(const std::string& className, Creator creator)
    {
        if (!creator)
        {
            return false;
        }

        return _map.emplace(className, creator).second;
    }

    Component* Create(const std::string& className, Targs&&... args)
    {
        auto iter = _map.find(className);
        if (iter == _map.end())
        {
            return nullptr;
        }

        return iter->second(std::forward<Targs>(args)...);
    }

private:
    std::unordered_map<std::string, Creator> _map;
};

这里的 Targs... 表示构造函数参数类型。于是下面几个工厂不是同一个类型:

cpp 复制代码
ComponentFactory<std::string>
ComponentFactory<int>
ComponentFactory<int, std::string>

它们会被编译器实例化成不同的类,并且各自拥有独立的 _map

可以把它理解成编译器生成了几套不同的工厂:

cpp 复制代码
// 伪代码
class ComponentFactory_string
{
    std::unordered_map<std::string, std::function<Component*(std::string)>> _map;
};

class ComponentFactory_int
{
    std::unordered_map<std::string, std::function<Component*(int)>> _map;
};

class ComponentFactory_int_string
{
    std::unordered_map<std::string, std::function<Component*(int, std::string)>> _map;
};

所以:

cpp 复制代码
Test1(std::string)

应该注册进:

cpp 复制代码
ComponentFactory<std::string>

而:

cpp 复制代码
Test2(int)

应该注册进:

cpp 复制代码
ComponentFactory<int>

这一点非常关键。模板参数不同,工厂类型就不同,内部注册表也不同。


6. 自动注册器:RegistCreator<T, Targs...>

如果每个类都手写创建函数,会很繁琐:

cpp 复制代码
class Test1 : public Component
{
public:
    static Component* CreateObject(std::string name)
    {
        return new Test1(name);
    }
};

每个类都要写类似的 CreateObject,重复代码很多。我们可以用模板自动生成这个函数:

cpp 复制代码
template<typename T, typename... Targs>
class RegistCreator
{
public:
    explicit RegistCreator(const std::string& className)
    {
        ComponentFactory<Targs...>::Instance()->Regist(
            className,
            &RegistCreator<T, Targs...>::CreateObject
        );
    }

    static Component* CreateObject(Targs&&... args)
    {
        return new T(std::forward<Targs>(args)...);
    }
};

使用方式:

cpp 复制代码
RegistCreator<Test1, std::string> registTest1("Test1");
RegistCreator<Test2, int> registTest2("Test2");

这两行代码分别表示:

text 复制代码
"Test1" -> new Test1(std::string)
"Test2" -> new Test2(int)

更具体地说:

cpp 复制代码
RegistCreator<Test1, std::string> registTest1("Test1");

会把 Test1 注册进:

cpp 复制代码
ComponentFactory<std::string>

并且创建函数是:

cpp 复制代码
RegistCreator<Test1, std::string>::CreateObject

它最终执行:

cpp 复制代码
new Test1(std::forward<std::string>(args)...);

这里 RegistCreator 的两个模板参数含义是:

cpp 复制代码
template<typename T, typename... Targs>

T 是要注册的类,例如 Test1Test2

Targs... 是这个类构造函数需要的参数类型,例如 std::stringintint, std::string


7. 封装统一入口:CreateComponent

为了避免外部直接使用具体的 ComponentFactory<Targs...>,可以再封装一层:

cpp 复制代码
template<typename... Targs>
Component* CreateComponent(const std::string& className, Targs&&... args)
{
    return ComponentFactory<Targs...>::Instance()->Create(
        className,
        std::forward<Targs>(args)...
    );
}

使用时:

cpp 复制代码
CreateComponent("Test1", std::string("hello"));
CreateComponent("Test2", 100);

编译器会根据实参自动推导 Targs...

对于:

cpp 复制代码
CreateComponent("Test1", std::string("hello"));

会推导出:

cpp 复制代码
Targs... = std::string

于是进入:

cpp 复制代码
ComponentFactory<std::string>

对于:

cpp 复制代码
CreateComponent("Test2", 100);

会推导出:

cpp 复制代码
Targs... = int

于是进入:

cpp 复制代码
ComponentFactory<int>

也就是说,CreateComponent 只是统一入口,真正决定进入哪个工厂的,是构造参数的类型列表。


8. 创建请求和参数来自网络协议

到这里,我们已经可以写:

cpp 复制代码
CreateComponent("Test1", std::string("hello"));
CreateComponent("Test2", 100);

但是游戏服务器中的创建请求往往不是直接写在 C++ 代码里的,而是来自网络协议或配置文件。

例如 protobuf 中可能定义:

proto 复制代码
message CreateComponentParam {
    enum ParamType {
        INT = 0;
        STRING = 1;
    }

    ParamType type = 1;
    string data = 2;
}

message CreateComponent {
    string class_name = 1;
    repeated CreateComponentParam params = 2;
}

运行时收到:

text 复制代码
class_name = "Player"
params = [
    { type = INT,    data = "1001" },
    { type = STRING, data = "ayanami" }
]

目标是最终调用:

cpp 复制代码
CreateComponent("Player", 1001, std::string("ayanami"));

这里出现了一个 C++ 中非常典型的矛盾:

  1. 协议参数是运行时才知道的。
  2. 模板参数必须在编译期确定(编译期模板实例化)。
  3. 构造函数调用要求参数类型在编译期明确。

因此,我们需要在运行时参数和编译期类型系统之间搭桥

大致流程是:

text 复制代码
协议参数列表
        ↓
DataInfo 运行时表示
        ↓
std::tuple 逐步收集参数
        ↓
DynamicCall 递归模板
        ↓
ComponentFactory<Targs...>
        ↓
Create(className, args...)

9. DataInfo:保存运行时参数

先用一个简单结构表示协议中的参数:

cpp 复制代码
#include <string>
#include <utility>

struct DataInfo
{
    enum class Type
    {
        Int,
        String
    };

    Type type;
    int intVal = 0;
    std::string strVal;

    explicit DataInfo(int value)
        : type(Type::Int), intVal(value)
    {
    }

    explicit DataInfo(std::string value)
        : type(Type::String), strVal(std::move(value))
    {
    }
};

它是协议参数的运行时表示。

如果协议中解析出一个整数:

cpp 复制代码
DataInfo info(1001);

如果协议中解析出一个字符串:

cpp 复制代码
DataInfo info(std::string("ayanami"));

多个参数可以放进 std::list<DataInfo>

cpp 复制代码
#include <list>

std::list<DataInfo> params;
params.emplace_back(1001);
params.emplace_back(std::string("ayanami"));

此时 params 表示的运行时参数列表是:

text 复制代码
[int:1001, string:"ayanami"]

10. 为什么需要 std::tuple

运行时参数列表是:

text 复制代码
[int:1001, string:"ayanami"]

但是 C++ 工厂需要的是一个编译期类型列表:

cpp 复制代码
ComponentFactory<int, std::string>

也就是说,我们需要把运行时读到的参数逐步收集成:

cpp 复制代码
std::tuple<int, std::string>

然后再把这个 tuple 展开成普通函数参数:

cpp 复制代码
Create("Player", 1001, std::string("ayanami"));

std::tuple 的作用是保存一组类型不同的参数。例如:

cpp 复制代码
auto t1 = std::make_tuple();
auto t2 = std::tuple_cat(t1, std::make_tuple(1001));
auto t3 = std::tuple_cat(t2, std::make_tuple(std::string("ayanami")));

t1 的类型是:

cpp 复制代码
std::tuple<>

t2 的类型是:

cpp 复制代码
std::tuple<int>

t3 的类型是:

cpp 复制代码
std::tuple<int, std::string>

注意,这里的类型是在编译期确定的。虽然运行时在判断参数类型,但每次进入某个分支后,编译器都能确定追加后的 tuple 类型。


11. DynamicCall:递归解析运行时参数

接下来用一个递归模板不断读取 DataInfo,并把参数追加到 tuple 中。

核心代码如下:

cpp 复制代码
#include <list>
#include <tuple>

template<int ICount>
struct DynamicCall
{
    template<typename... Targs>
    static Component* Invoke(
        const std::string& className,
        std::tuple<Targs...> t1,
        std::list<DataInfo>& params
    )
    {
        if (params.empty())
        {
            return ComponentFactory<Targs...>::Instance()->CreateWithTuple(className, t1);
        }

        DataInfo info = params.front();
        params.pop_front();

        if (info.type == DataInfo::Type::Int)
        {
            auto t2 = std::tuple_cat(t1, std::make_tuple(info.intVal));
            return DynamicCall<ICount - 1>::Invoke(className, t2, params);
        }

        if (info.type == DataInfo::Type::String)
        {
            auto t2 = std::tuple_cat(t1, std::make_tuple(info.strVal));
            return DynamicCall<ICount - 1>::Invoke(className, t2, params);
        }

        return nullptr;
    }
};

假设运行时收到:

cpp 复制代码
params = [1, "test"]

初始调用:

cpp 复制代码
DynamicCall<5>::Invoke("C1", std::make_tuple(), params);

调用过程可以展开为:

text 复制代码
DynamicCall<5>::Invoke(tuple<>)
    读到 int
    生成 tuple<int>

DynamicCall<4>::Invoke(tuple<int>)
    读到 string
    生成 tuple<int, string>

DynamicCall<3>::Invoke(tuple<int, string>)
    params 为空
    调用 ComponentFactory<int, string>::Create(...)

最终会进入:

cpp 复制代码
ComponentFactory<int, std::string>

然后根据 "C1" 查找对应创建函数。

这套机制看起来像运行时动态调用,但底层仍然依赖 C++ 模板在编译期生成不同参数组合的函数实例


12. 把 tuple 展开成普通函数参数

现在有了:

cpp 复制代码
std::tuple<int, std::string>

但工厂中的 Create 需要的是:

cpp 复制代码
Create(className, intValue, stringValue);

所以需要把 tuple 展开。

12.1 C++14:std::index_sequence

C++14 常用 std::index_sequence

cpp 复制代码
#include <tuple>
#include <utility>

template<typename... Targs>
class ComponentFactory
{
public:
    Component* CreateWithTuple(
        const std::string& className,
        const std::tuple<Targs...>& args
    )
    {
        return CreateWithTupleImpl(
            className,
            args,
            std::index_sequence_for<Targs...>{}
        );
    }

private:
    template<std::size_t... Index>
    Component* CreateWithTupleImpl(
        const std::string& className,
        const std::tuple<Targs...>& args,
        std::index_sequence<Index...>
    )
    {
        return Create(
            className,
            std::get<Index>(args)...
        );
    }
};

如果 Targs... 是:

cpp 复制代码
int, std::string

那么:

cpp 复制代码
std::index_sequence_for<Targs...>{}

会生成:

cpp 复制代码
std::index_sequence<0, 1>

于是:

cpp 复制代码
std::get<Index>(args)...

展开为:

cpp 复制代码
std::get<0>(args), std::get<1>(args)

也就是:

cpp 复制代码
Create(className, std::get<0>(args), std::get<1>(args));

12.2 C++17:std::apply

C++17 可以用 std::apply 简化:

cpp 复制代码
Component* CreateWithTuple(
    const std::string& className,
    const std::tuple<Targs...>& args
)
{
    return std::apply(
        [&](auto&&... unpackedArgs) -> Component*
        {
            return Create(className, std::forward<decltype(unpackedArgs)>(unpackedArgs)...);
        },
        args
    );
}

std::apply 的作用就是把 tuple 拆开,并把其中的元素作为普通参数传给函数对象。


13. 模板会导致编译时间会变长

DynamicCall 看起来只是在运行时递归处理参数,但它还有一个隐藏成本:大量编译期模板实例化。

考虑这段代码:

cpp 复制代码
if (info.type == DataInfo::Type::Int)
{
    auto t2 = std::tuple_cat(t1, std::make_tuple(info.intVal));
    return DynamicCall<ICount - 1>::Invoke(className, t2, params);
}
else
{
    auto t2 = std::tuple_cat(t1, std::make_tuple(info.strVal));
    return DynamicCall<ICount - 1>::Invoke(className, t2, params);
}

info.type 是运行时变量,不是编译期常量。

这意味着编译器不能只编译某一个分支。它必须同时编译 if 分支和 else 分支,因为运行时两个分支都有可能被执行。

假设支持两种参数类型:

cpp 复制代码
int
std::string

最大参数个数为 3,那么编译器可能生成类似下面这些函数实例:

text 复制代码
DynamicCall<2>::Invoke<int>
DynamicCall<2>::Invoke<std::string>

DynamicCall<1>::Invoke<int, int>
DynamicCall<1>::Invoke<int, std::string>
DynamicCall<1>::Invoke<std::string, int>
DynamicCall<1>::Invoke<std::string, std::string>

DynamicCall<0>::Invoke<int, int, int>
DynamicCall<0>::Invoke<int, int, std::string>
DynamicCall<0>::Invoke<int, std::string, int>
DynamicCall<0>::Invoke<int, std::string, std::string>
DynamicCall<0>::Invoke<std::string, int, int>
DynamicCall<0>::Invoke<std::string, int, std::string>
DynamicCall<0>::Invoke<std::string, std::string, int>
DynamicCall<0>::Invoke<std::string, std::string, std::string>

组合数量近似为:

text 复制代码
类型数量 ^ 最大参数个数

如果只支持 2 种类型、最多 3 个参数,组合还不算多。如果支持 6 种类型、最多 6 个参数,组合数量会迅速膨胀:

text 复制代码
6^6 = 46656

每一种组合都可能导致 DynamicCallComponentFactoryCreateWithTuple 等模板函数实例化。代码看起来不多,但编译器实际生成和检查的代码非常多。

这就是模板动态调用方案容易导致编译时间变长、二进制膨胀的原因。


14. DynamicCall<0>的作用

上面的递归模板有一行非常关键:

cpp 复制代码
DynamicCall<ICount - 1>::Invoke(className, t2, params);

如果只有通用模板:

cpp 复制代码
template<int ICount>
struct DynamicCall
{
    // ...
};

那么当 ICount == 0 时,编译器仍然会继续实例化:

cpp 复制代码
DynamicCall<-1>
DynamicCall<-2>
DynamicCall<-3>

这会形成没有终点的模板递归。最终结果通常是达到模板实例化深度限制,或者报出编译器资源不足。

所以必须提供一个终止特化:

cpp 复制代码
template<>
struct DynamicCall<0>
{
    template<typename... Targs>
    static Component* Invoke(
        const std::string& className,
        std::tuple<Targs...> t1,
        std::list<DataInfo>& params
    )
    {
        if (!params.empty())
        {
            throw std::runtime_error("too many constructor params");
        }

        return ComponentFactory<Targs...>::Instance()->CreateWithTuple(className, t1);
    }
};

DynamicCall<0> 是编译期递归的终点。它不再调用:

cpp 复制代码
DynamicCall<ICount - 1>

因此不会继续生成:

cpp 复制代码
DynamicCall<-1>

这里要注意一个容易混淆的点:

cpp 复制代码
if (params.empty())
{
    return ...;
}

这个判断只能终止运行时递归,不能终止编译期模板递归

原因是 params.empty() 是运行时条件。编译器在编译模板时,不能根据它判断某个分支一定不会执行。因此,只要代码中存在:

cpp 复制代码
DynamicCall<ICount - 1>

编译器就可能继续实例化下去。

真正阻断编译期递归的,是模板特化:

cpp 复制代码
template<>
struct DynamicCall<0>
{
    // 不再调用 DynamicCall<-1>
};

15. 运行时递归和编译期递归的区别

DynamicCall 中同时存在两种递归,必须区分清楚。

运行时递归是:

cpp 复制代码
if (params.empty())
{
    return;
}

它根据程序运行时的数据决定是否继续。

编译期模板递归是:

cpp 复制代码
DynamicCall<ICount - 1>

它会在编译阶段触发新的模板实例生成。

两者对比如下:

对比项 运行时递归 编译期模板递归
发生时间 程序运行时 编译阶段
判断依据 运行时数据 模板参数 / 编译期常量
终止方式 if / return 模板特化 / if constexpr
主要代价 运行时调用成本 编译时间 / 代码膨胀
示例 params.empty() DynamicCall<0>

工程上最容易犯的错误是:以为 if (params.empty()) return; 可以阻止模板继续展开。实际上它不行。只要模板代码中还写着 DynamicCall<ICount - 1>,编译器就会尝试实例化它。


16. 完整示例代码

下面给出一份完整的 C++17 示例。为了便于工程化管理,这里使用 std::unique_ptr<Component> 表达对象所有权。

cpp 复制代码
#include <functional>
#include <iostream>
#include <list>
#include <memory>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>

class Component
{
public:
    virtual ~Component() = default;
};

class Test1 : public Component
{
public:
    explicit Test1(std::string name)
    {
        std::cout << "create Test1, name = " << name << std::endl;
    }
};

class Test2 : public Component
{
public:
    explicit Test2(int id)
    {
        std::cout << "create Test2, id = " << id << std::endl;
    }
};

class Test3 : public Component
{
public:
    Test3(int id, std::string name)
    {
        std::cout << "create Test3, id = " << id
                  << ", name = " << name << std::endl;
    }
};

template<typename... Targs>
class ComponentFactory
{
public:
    using Creator = std::function<std::unique_ptr<Component>(Targs...)>;

    static ComponentFactory* Instance()
    {
        static ComponentFactory instance;
        return &instance;
    }

    bool Regist(const std::string& className, Creator creator)
    {
        if (!creator)
        {
            return false;
        }

        return creators_.emplace(className, std::move(creator)).second;
    }

    std::unique_ptr<Component> Create(const std::string& className, Targs... args)
    {
        auto iter = creators_.find(className);
        if (iter == creators_.end())
        {
            return nullptr;
        }

        return iter->second(std::forward<Targs>(args)...);
    }

    std::unique_ptr<Component> CreateWithTuple(
        const std::string& className,
        const std::tuple<Targs...>& args
    )
    {
        return std::apply(
            [&](auto&&... unpackedArgs) -> std::unique_ptr<Component>
            {
                return Create(
                    className,
                    std::forward<decltype(unpackedArgs)>(unpackedArgs)...
                );
            },
            args
        );
    }

private:
    std::unordered_map<std::string, Creator> creators_;
};

template<typename T, typename... Targs>
class RegistCreator
{
public:
    explicit RegistCreator(const std::string& className)
    {
        ComponentFactory<Targs...>::Instance()->Regist(
            className,
            &RegistCreator<T, Targs...>::CreateObject
        );
    }

    static std::unique_ptr<Component> CreateObject(Targs... args)
    {
        return std::make_unique<T>(std::forward<Targs>(args)...);
    }
};

template<typename... Targs>
std::unique_ptr<Component> CreateComponent(const std::string& className, Targs... args)
{
    return ComponentFactory<Targs...>::Instance()->Create(
        className,
        std::forward<Targs>(args)...
    );
}

struct DataInfo
{
    enum class Type
    {
        Int,
        String
    };

    Type type;
    int intVal = 0;
    std::string strVal;

    explicit DataInfo(int value)
        : type(Type::Int), intVal(value)
    {
    }

    explicit DataInfo(std::string value)
        : type(Type::String), strVal(std::move(value))
    {
    }
};

template<int ICount>
struct DynamicCall
{
    template<typename... Targs>
    static std::unique_ptr<Component> Invoke(
        const std::string& className,
        std::tuple<Targs...> t1,
        std::list<DataInfo>& params
    )
    {
        if (params.empty())
        {
            return ComponentFactory<Targs...>::Instance()->CreateWithTuple(className, t1);
        }

        DataInfo info = std::move(params.front());
        params.pop_front();

        if (info.type == DataInfo::Type::Int)
        {
            auto t2 = std::tuple_cat(t1, std::make_tuple(info.intVal));
            return DynamicCall<ICount - 1>::Invoke(className, t2, params);
        }

        if (info.type == DataInfo::Type::String)
        {
            auto t2 = std::tuple_cat(t1, std::make_tuple(std::move(info.strVal)));
            return DynamicCall<ICount - 1>::Invoke(className, t2, params);
        }

        return nullptr;
    }
};

template<>
struct DynamicCall<0>
{
    template<typename... Targs>
    static std::unique_ptr<Component> Invoke(
        const std::string& className,
        std::tuple<Targs...> t1,
        std::list<DataInfo>& params
    )
    {
        if (!params.empty())
        {
            throw std::runtime_error("too many constructor params");
        }

        return ComponentFactory<Targs...>::Instance()->CreateWithTuple(className, t1);
    }
};

int main()
{
    RegistCreator<Test1, std::string> r1("Test1");
    RegistCreator<Test2, int> r2("Test2");
    RegistCreator<Test3, int, std::string> r3("Test3");

    {
        auto obj = CreateComponent("Test1", std::string("hello"));
        if (!obj)
        {
            std::cout << "create Test1 failed" << std::endl;
        }
    }

    {
        auto obj = CreateComponent("Test2", 1001);
        if (!obj)
        {
            std::cout << "create Test2 failed" << std::endl;
        }
    }

    {
        std::list<DataInfo> params;
        params.emplace_back(1001);
        params.emplace_back(std::string("player_001"));

        auto obj = DynamicCall<5>::Invoke("Test3", std::make_tuple(), params);
        if (!obj)
        {
            std::cout << "create Test3 failed" << std::endl;
        }
    }

    return 0;
}

可能输出:

text 复制代码
create Test1, name = hello
create Test2, id = 1001
create Test3, id = 1001, name = player_001

这份代码展示了完整流程:

text 复制代码
注册类
    ↓
建立 className 到 creator 的映射
    ↓
协议参数进入 DataInfo
    ↓
DynamicCall 生成 tuple<Targs...>
    ↓
ComponentFactory<Targs...> 查找创建函数
    ↓
调用构造函数
    ↓
返回 unique_ptr<Component>

17. 优点

第一,可以通过字符串创建对象。对象创建可以来自配置、网络协议、脚本或 GM 命令。

第二,可以统一对象生命周期。对象不再散落在业务代码里直接 new,而是通过工厂和 EntitySystem 创建。

第三,便于接入 Actor 模型。外部线程只投递创建消息,真正创建动作可以放到目标 Actor / EntitySystem 所属线程执行。

第四,可以支持不同构造函数参数。ComponentFactory<Targs...> 根据参数类型生成不同工厂,每个工厂维护自己的创建表。

第五,注册逻辑可以复用。RegistCreator<T, Targs...> 避免了每个类重复手写 CreateObject


18. 缺点

第一,它不是 C++ 原生反射,只是手写注册表。没有注册的类无法创建。

第二,注册名需要维护。如果代码中注册 "Player",协议中传 "player",就会创建失败。

第三,不建议依赖 typeid(T).name() 作为跨平台类名。不同编译器、不同平台、不同构建配置下,typeid(T).name() 的结果可能不同。工程中更推荐显式注册:

cpp 复制代码
RegistCreator<Player, int, std::string> r("Player");

第四,参数类型组合会导致模板实例膨胀。支持的参数类型越多、最大参数个数越大,编译时间和二进制体积越容易失控。

第五,支持的参数类型必须提前写入 DataInfoDynamicCall。如果后来要支持 floatbooluint64_t,需要扩展参数表示和分支逻辑。

第六,错误可能运行时才暴露。例如注册的是:

cpp 复制代码
RegistCreator<Test1, std::string> r("Test1");

但创建时传的是:

cpp 复制代码
CreateComponent("Test1", 100);

程序会去 ComponentFactory<int> 中查找 "Test1",自然找不到。

第七,如果使用裸指针,生命周期容易混乱。实际工程中应该明确所有权,例如返回 std::unique_ptr<Component>,或者由 EntitySystem 统一接管。

相关推荐
王老师青少年编程1 小时前
2026年6月GESP真题及题解(C++七级):消消乐
数据结构·c++·算法·真题·gesp·2026年6月
海清河晏1112 小时前
数据结构 | 二叉搜索树
数据结构·c++·visual studio
想你依然心痛2 小时前
Linux用户空间与内核空间通信:netlink、ioctl、mmap 零拷贝与性能对比
linux·运维·服务器
程序员在囧途2 小时前
likeadmin-api API 算力超市怎么做供应商切换?统一鉴权、task_id 和 callback_url 才能稳交付
java·服务器·数据库·开放api·likeadmin-api·api算力超市
盐焗鹌鹑蛋2 小时前
【C++】继承
开发语言·c++
栈溢出了3 小时前
Java Lambda 表达式笔记
java·开发语言·intellij-idea
会周易的程序员3 小时前
使用 pybind11 封装 C++ 日志库 microLog 为 Python 模块
java·c++·python·物联网·架构·日志·aiot
WZF-Sang3 小时前
网络基础——2
服务器·网络·c++·学习·网络编程·php
zhixingheyi_tian3 小时前
一份GC日志的解读
java·开发语言