【问题标题】:Instantiation of class by classname通过类名实例化类
【发布时间】:2014-11-18 12:45:45
【问题描述】:

我有多个类共享一个公共基类,如下所示:

class Base {};

class DerivedA : public Base {};
class DerivedB : public Base {};
class DerivedC : public Base {};

现在,我需要知道在运行时要实例化这些派生类中的哪些(基于输入)。例如,如果输入是"DerivedA",我需要创建一个DerivedA 对象。输入不一定是字符串,也可以是整数 - 关键是有某种键,我需要一个值来匹配键。

但问题是,我如何实例化该类? C++ 没有像 C# 或 Java 那样的内置反射。我发现的一个普遍建议的解决方案是使用这样的工厂方法:

Base* create(const std::string& name) {
    if(name == "DerivedA") return new DerivedA();
    if(name == "DerivedB") return new DerivedB();
    if(name == "DerivedC") return new DerivedC();
}

如果只有几个类,这就足够了,但如果有几十个或几百个派生类,这会变得很麻烦并且可能很慢。我可以很容易地自动化地图创建过程来生成std::map<std::string, ***>,但我不知道要存储什么作为值。 AFAIK,不允许指向构造函数的指针。同样,如果我使用此映射创建工厂,我仍然需要为每种类型编写工厂方法,这比上面的示例更加繁琐。

什么是处理这个问题的有效方法,尤其是当有很多派生类时?

【问题讨论】:

标签: c++ instantiation


【解决方案1】:

您始终可以存储 std::function<Base*()>,因为您始终从您的 create 函数返回指向 Base 的指针:

class Base {};

class DerivedA : public Base {};
class DerivedB : public Base {};
class DerivedC : public Base {};

Base* create(const std::string& type)
{
    static std::map<std::string, std::function<Base*()>> type_creator_map =
    {
        {"DerivedA", [](){return new DerivedA();}},
        {"DerivedB", [](){return new DerivedB();}},
        {"DerivedC", [](){return new DerivedC();}}
    };

    auto it = type_creator_map.find(type);
    if(it != type_creator_map.end())
    {
        return it->second();
    }

    return nullptr;
}

正如 Angew 所建议的,您应该返回 std::unique_ptr 而不是原始指针。如果create 函数的用户想要一个原始指针或std::shared_ptr,他/她可以“抓取”原始指针并使用它。

更新:

Next 方法提供了一种方便的半自动方式,无需更改旧代码即可注册新类型。

我不建议使用它,因为它取决于链接器(创建全局变量的时刻可能会延迟),它们编译代码的方式(可执行文件、静态库、动态库),它在 @987654328 之前分配内存@ 开始并创建奇怪的命名全局变量。

只有当你真的知道你在做什么并且知道你在什么平台上使用代码时才使用它!

class Base {};

std::map<std::string, std::function<Base*()>>& get_type_creator_map()
{
    static std::map<std::string, std::function<Base*()>> type_creator_map;
    return type_creator_map;
}

template<typename T>
struct RegisterTypeHelper
{
    RegisterTypeHelper(const std::string& id)
    {
        get_type_creator_map()[id] = [](){return new T();};
    }
};

Base* create(const std::string& type)
{
    auto& type_creator_map = get_type_creator_map();
    auto it = type_creator_map.find(type);
    if(it != type_creator_map.end())
    {
        return it->second();
    }

    return nullptr;
}

#define RegisterType(Type) static RegisterTypeHelper<Type> register_type_global_##Type(#Type)

class DerivedA : public Base {};
RegisterType(DerivedA);

class DerivedB : public Base {};
RegisterType(DerivedB);

class DerivedC : public Base {};
RegisterType(DerivedC);

【讨论】:

  • 这正是我所寻找的——我知道我可以使用 lambdas 以某种方式解决这个问题!当然,这不是自动的,但就像 Andreas 建议的那样,我可以编写一个小工具来解析标题并使用解析的类名创建映射。谢谢!
  • 它可以是(半)自动的,具体取决于链接器以及您是否接受使用一些全局变量/单例。我将用一个简短的示例更新答案
  • 尾随返回类型是不必要的。 std::function 接受其返回类型可以隐式转换为模板参数中指定的返回类型的任何内容。
  • @T.C.我不知道。谢谢!我编辑了我的答案。
  • 请注意,第二个解决方案虽然非常清晰,但会遭受初始化“惨败”的顺序。如果您在一个 T.U. 中定义 register_type_global_xxx。并且您尝试从另一个 T.U. 创建相同类型的对象。在调用main 之前,您可能仍会得到nullptr
【解决方案2】:

解决这个问题的一种方法是使用设计模式Prototype

基本上,您不会通过直接初始化来创建派生类对象,而是通过克隆原型来创建。您的create() 函数实际上是Factory method 设计模式的实现。您可以在实现中使用 Prototype,如下所示:

class Base
{
public:
  virtual ~Base() {}
  virtual Base* clone() = 0;
};

class DerivedA : public Base
{
public:
  virtual DerivedA* clone() override { return new DerivedA; }
};


Base* create(const std::string &name)
{
  static std::map<std::string, Base*> prototypes {
    { "DerivedA", new DerivedA },
    { "DerivedB", new DerivedB },
    { "DerivedC", new DerivedC }
  };
  return prototypes[name]->clone();
}

为简洁起见,示例中省略了错误检查。

在实际项目中,您当然应该使用智能指针(例如std::unique_ptr)而不是原始指针来管理对象的生命周期。

【讨论】:

  • 相对于 OPs 解决方案的优势在哪里?您仍然必须将类名的字符串映射到类本身。
  • @MatthiasB 引用 OP:“我可以很容易地自动化地图创建过程以生成 std::map&lt;std::string, ***&gt;,但 我不知道要存储什么作为值。” (强调我的)。我正在展示要存储什么作为值,以及如何使用它。
  • 我不认为需要原型。此外,您需要将prototypes 中的对象newed 安排为deleted
  • @juanchopanza 那么你会存储什么而不是原型呢?关于delete,这就是为什么我添加了关于智能指针的评论。
  • 我会存储返回指向对象的(智能)指针的函数。
【解决方案3】:

我可以很容易地自动化地图创建过程以生成 std​​::map,但我不知道要存储什么作为值

您需要将工厂方法存储为值,例如一个创建类实例的静态方法:

class Base {};

class DerivedA : public Base {
public:
    static Base* create();

    ...
}

...

Base* DerivedA::create() {
    return new DerivedA();
}

然后您可以通过类似的地图实现名称/查找

typedef Base* (*FACTORY_FUNCTION)();
std::map<std::string, FACTORY_FUNCTION> factories;

...

factories["ClassA"] = ClassA::create;

如果我使用这个映射做一个工厂,我仍然需要为每种类型编写一个工厂方法

由于这些工厂方法非常简单,您可以通过简单的代码生成工具(例如使用简单的 shell 脚本)自动化创建它们。您可以维护一个类列表,或者从头文件中检索此列表(例如,通过 grepping 获取 class 关键字并检索后续的类名,或者使用一些可以正确解析标头的分析工具甚至更好文件)。

利用这些信息,您可以自动创建必要的代码来自动将工厂方法添加到每个类。使用相同的方法,您还可以生成需要调用一次的注册函数,以便您的对象被注册。

【讨论】:

  • 我没有投反对票,也看不出有任何理由这样做。只是一个评论:为什么类应该包含一个静态方法来实例化自己?这似乎是一个不必要的限制,这使得解决方案不太通用(即它只适用于具有此静态成员的类。)
  • @juanchopanza 所以你的问题是为什么工厂方法是类的 静态成员 - 它同样也可以是类外的公共方法或静态成员在一个单独的类中,它提供了所有必要的工厂函数,对吧?
  • 我更多地考虑任何不带参数并返回(智能)指针的可调用对象。它可以是一个函数、一个仿函数、一个 lambda,任何东西。
  • @juanchopanza 是的——我想当时我实现上述方法时的想法只是将工厂封装在类中(本质上,“类应该知道如何创建自己”)......
猜你喜欢
  • 2019-12-08
  • 2020-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-26
  • 2011-10-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多