2009-07-08 114 views
27

想象一下,我有一堆C++相关的类(所有扩展了相同的基类和提供相同的构造函数),我在一个公共头文件(我包括)中声明,以及它们在一些其他文件中的实现(我编译和静态链接为我的程序构建的一部分)。从名称实例化类?

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

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

#ifndef CLASS_NAME 
#define CLASS_NAME MyDefaultClassToUse 
#endif 

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

是它的唯一有价值的方法?

回答

39

这是使用常用解决的问题Registry Pattern

这就是 注册模式描述的情况:

对象需要联系另一个 对象,只知道对象的名称 或 提供的服务名称,但不包括如何与其联系。 提供一个服务,该服务采用对象,服务或角色的名称 ,并且 返回一个远程代理,该代理将 与如何与 联系的知识联系到指定的对象。

这是基本相同的发布/发现,形成服务 导向架构(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使用提供了可以在运行时通过名字来创建组件类注册表模式 - 注册是使用类似下面的代码一样简单:

OBJECT_ENTRY_AUTO(someClassID, SomeClassName); 

这个宏被放置在某处你的头文件,魔术它会导致在COM服务器启动时在单例中注册。

2

在C++中,这个决定必须在编译时做出。

在编译时你可以使用一个typedef,而不是玻璃陶瓷:

typedef DefaultClass MyDefaultClassToUse; 

这相当于,避免了宏(宏坏;-))。

如果要在运行期间做出决定,则需要编写自己的代码来支持它。 simples解决方案是一个函数,它测试字符串并实例化相应的类。

扩展版本(允许独立代码段注册他们的类)将是map<name, factory function pointer>

+1

“扩展版本(允许独立的代码段注册它们的类)将是一个映射<名称,工厂函数指针>。” 从类定义中可以将自己注册到该映射中,假设该映射位于其他地方的Factory中? 在Java中,我可以/使用静态构造函数来完成它。实际上,我不想为我编写的每个新子类修改Factory的代码。 – puccio 2009-07-08 08:15:09

5

为什么不使用对象工厂?

最简单的形式:

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; 
} 
1

您提到了两种可能性 - 命令行和汇编宏,但每种解决方案都有很大的不同。

如果选择是通过编译宏来完成的,那么这是一个可以用#defines和#ifdefs等解决的简单问题。您提出的解决方案与任何解决方案一样好。

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

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)); 

编辑 - 在一般请求:)有点返工,使事情看起来更平滑(学分石免费谁没有可能要添加一个答案本人)

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. 
+0

Yeargs,最后的'变革'号召让我的眼睛受伤。 % - ) – 2009-07-08 09:56:44

0

在过去,我已经实现了Factory模式,使得类可以在运行时进行自我注册,而工厂本身不必具体了解它们。关键是要使用称为(IIRC)的“初始化附件”的非标准编译器功能,其中您在实现文件中为每个类声明一个虚拟静态变量(例如bool),并通过调用注册来初始化它常规。

在这个方案中,每个类都必须#include包含它的工厂的头文件,但工厂除了接口类之外什么都不知道。你可以从你的版本中直接添加或者删除实现类,并且不用修改代码就可以重新编译。

问题是只有一些编译器通过初始化支持连接-IIRC其他人在第一次使用时初始化文件范围变量(与函数本地静态工作方式相同),由于从不访问虚拟变量,工厂地图将始终为空。

我感兴趣的编译器(MSVC和GCC)确实支持这一点,所以对我来说这不是问题。你必须自己决定这个解决方案是否适合你。

1

虽然现在问题已经存在了四年多了,但它仍然有用。因为在编译和链接主代码文件时调用未知的新代码是当今非常常见的情况。根本没有提到这个问题的一个解决方案。因此,我喜欢将观众指向不是用C++构建的另一种解决方案。 C++本身没有能力表现得像从Java已知的Class.forName()或从.NET已知的Activator.CreateInstance(type)。由于上述原因,虚拟机没有对JIT代码进行实时监控。但无论如何,LLVM,低级虚拟机,为您提供所需的工具和库以读入编译的库。基本上,你需要执行两个步骤:

  1. 编译C/C++源代码,你喜欢动态实例。你需要将它编译成bitcode,所以你最终得到了一个,比如foo.bc。你可以用铿锵做到这一点,并提供一个编译器开关:clang -emit-llvm -o foo.bc -c foo.c
  2. 你需要再使用ParseIRFile()方法从llvm/IRReader/IRReader.h解析foo.bc文件,以获得相关的功能(LLVM本身只知道用作位码是直接抽象对于更高级别的中间表示,比如Java字节码,CPU操作码并不太熟悉)。有关更完整的代码说明,请参阅此article的实例。

在上面勾画了这些步骤之后,您还可以从C++动态调用其他之前未知的函数和方法。