从类名实例化类?

30

假设我有一些与C++相关的类(它们都扩展了相同的基类并提供了相同的构造函数),我在一个公共头文件中声明它们(我包含该头文件),它们的实现分别在其他文件中(我将其编译并静态链接为程序构建的一部分)。

我想能够通过传递名称来实例化其中之一,该名称是必须作为参数传递给我的程序的(可以作为命令行或编译宏)。

我所看到的唯一可能的解决方案是使用宏:

#ifndef CLASS_NAME
#define CLASS_NAME MyDefaultClassToUse
#endif

BaseClass* o = new CLASS_NAME(param1, param2, ..);

这是唯一有价值的方法吗?

7个回答

42

这是一个通常使用注册表模式解决的问题:

注册表模式描述的情况如下:

对象需要联系另一个对象,只知道对象的名称或其提供的服务的名称,但不知道如何联系它。提供一个服务,接受对象、服务或角色的名称,并返回封装了如何联系命名对象的知识的远程代理。

这是构成面向服务架构(SOA)和OSGi中服务层基础的相同的基本发布/查找模型。

通常使用单例对象来实现注册表,单例对象在编译时或启动时被告知对象的名称和构造方式。然后,您可以使用它按需创建对象。

例如:

template<class T>
class Registry
{
    typedef boost::function0<T *> Creator;
    typedef std::map<std::string, Creator> Creators;
    Creators _creators;

  public:
    void register(const std::string &className, const Creator &creator);
    T *create(const std::string &className);
}

您可以这样注册对象的名称和创建函数:

Registry<I> registry;
registry.register("MyClass", &MyClass::Creator);

std::auto_ptr<T> myT(registry.create("MyClass"));

我们可以使用聪明的宏来简化这个过程,以便在编译时完成。ATL使用注册表模式来创建可以通过名称在运行时创建的CoClasses-注册过程就像使用以下代码一样简单:
OBJECT_ENTRY_AUTO(someClassID, SomeClassName);

这个宏会被放置在你的头文件中的某个地方,通过魔法使其在 COM 服务器启动时被注册到单例中。


10
一种实现方式是将类“名称”硬编码映射到工厂函数。使用模板可以使代码更短。STL可以使编码更容易。
#include "BaseObject.h"
#include "CommonClasses.h"

template< typename T > BaseObject* fCreate( int param1, bool param2 ) {
    return new T( param1, param2 );
}

typedef BaseObject* (*tConstructor)( int param1, bool param2 );
struct Mapping { string classname; tConstructor constructor; 
    pair<string,tConstructor> makepair()const { 
        return make_pair( classname, constructor ); 
    }
} mapping[] = 
{ { "class1", &fCreate<Class1> }
, { "class2", &fCreate<Class2> }
// , ...
};

map< string, constructor > constructors;
transform( mapping, mapping+_countof(mapping), 
    inserter( constructors, constructors.begin() ), 
    mem_fun_ref( &Mapping::makepair ) );

编辑--根据普遍要求:)稍微重新调整一下,使事情看起来更加流畅(感谢Stone Free,他可能不想自己添加答案)

typedef BaseObject* (*tConstructor)( int param1, bool param2 );
struct Mapping { 
    string classname; 
    tConstructor constructor; 

    operator pair<string,tConstructor> () const { 
        return make_pair( classname, constructor ); 
    }
} mapping[] = 
{ { "class1", &fCreate<Class1> }
, { "class2", &fCreate<Class2> }
// , ...
};

static const map< string, constructor > constructors( 
      begin(mapping), end(mapping) ); // added a flavor of C++0x, too.

年岁增长,最后的“transform”调用让我的眼睛疼。%-) - Frerich Raabe
你是正确的。你也可以不使用映射,而是用循环找到合适的构造函数。 - xtofl
实际上,您在上面的示例中已经拥有了必要的函数来完成完全相同的操作,但没有所有那些转换垃圾!在您的映射结构内,您有一个实用函数make_pair。您已经沿着正确的思路思考了。map构造函数接受指针类型的参数(实际上它们没有类型)。您可以利用这一点。最好使用整数类型来使用静态数据,因此您应将两个字符串成员更改为const char * s。然后将您的make_pair函数更改为mapped_type运算符 - Peter Nimmo
结构体映射 { const char *类名; tConstructor 构造函数; operator map<string,constructor>::value_type() const { return map<string,constructor>::value_type(类名, 构造函数); } } 映射 {[] = { { "class1", &fCreate<Class1> } , { "class2", &fCreate<Class2> } // , ... }; map< string, constructor > 构造器(映射,映射+_countof(映射)); 然后每当映射构造器解引用迭代器时,它将调用 value_type 将其强制转换为正确的类型!。 - Peter Nimmo

8

为什么不使用对象工厂?

简单来说:

BaseClass* myFactory(std::string const& classname, params...)
{
    if(classname == "Class1"){
        return new Class1(params...);
    }else if(...){
        return new ...;
    }else{
       //Throw or return null
    }
    return NULL;
}

2
在C++中,这个决定必须在编译时做出。在编译期间,您可以使用typedef而不是宏:
typedef DefaultClass MyDefaultClassToUse;

这是等效的,并避免了宏(宏不好;-))。

如果决策在运行时进行,您需要编写自己的代码来支持它。最简单的解决方案是编写一个测试字符串并实例化相应类的函数。

其扩展版本(允许独立的代码部分注册其类)将是一个map<name, factory function pointer>


1
一个扩展版本(允许独立的代码部分注册它们的类)将是一个map<名称,工厂函数指针>。假设此映射位于其他地方的工厂中,从类定义中是否可能将其自身注册到该映射中?在Java中,我可以/会使用static{}构造函数来完成这个操作。实际上,我不想为每个新的子类编写代码,修改工厂的代码。 - puccio

1

虽然这个问题已经存在四年多了,但它仍然非常有用。因为在编译和链接主代码文件时调用未知的新代码是现在非常普遍的情况。这个问题的一个解决方案根本没有被提到。因此,我想向观众指出一种不同类型的解决方案,它并没有内置在C++中。C++本身没有像Java中的Class.forName()或.NET中的Activator.CreateInstance(type)那样的行为能力。由于没有VM来实时JIT代码的原因。但无论如何,LLVM提供了您所需的工具和库来读取已编译的lib。基本上,您需要执行两个步骤:

  1. 编译您想要动态实例化的C/C++源代码。您需要将其编译为位码,以便最终得到一个名为foo.bc的文件。您可以使用clang并提供编译器开关:clang -emit-llvm -o foo.bc -c foo.c
  2. 然后,您需要使用llvm/IRReader/IRReader.h中的ParseIRFile()方法来解析foo.bc文件以获取相关函数(LLVM本身只知道函数,因为位码是CPU操作码的直接抽象,并且与Java字节码等更高级别的中间表示形式非常不同)。例如,请参考此article以获取更完整的代码描述。

在完成上述步骤之后,您可以从C++中动态调用其他先前未知的函数和方法。


1

你提到了两种可能性 - 命令行和编译宏,但每种情况的解决方案都大不相同。

如果选择是由编译宏做出的,那么这是一个简单的问题,可以通过 #defines 和 #ifdefs 等方式来解决。你提出的解决方案和其他任何方案一样好。

但是,如果选择是在运行时使用命令行参数进行的,那么你需要有一个工厂框架,能够接收一个字符串并创建适当的对象。这可以使用一个简单的静态 if().. else if()... else if()... 链来完成,其中包含所有可能性,或者可以是完全动态的框架,其中对象注册并被克隆以提供它们自己的新实例。


0
过去,我曾以这样的方式实现工厂模式,使得类可以在运行时自注册,而无需工厂本身特别知道它们。关键是使用一个非标准编译器特性,称为“初始化附加”,其中你为每个类在实现文件中声明一个虚拟静态变量(例如bool),并用调用注册例程的方式进行初始化。
在这种方案中,每个类都必须包含其工厂所在的头文件,但工厂只知道接口类,不知道其他任何东西。你可以随意添加或删除实现类,并重新编译,而不需要进行任何代码更改。
问题在于,只有一些编译器支持初始化附加 - 其他编译器会在第一次使用时初始化文件范围变量(与函数局部静态变量的工作方式相同),这对于这里没有帮助,因为虚拟变量永远不会被访问,工厂映射将始终为空。
我感兴趣的编译器(MSVC和GCC)支持这个功能,所以这对我来说不是问题。你必须自己决定是否适合这个解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接