想象我有一堆与C ++相关的类(所有扩展相同的基类并提供相同的构造函数),我在一个公共头文件(我包括)中声明,以及它们在其他一些文件中的实现(我编译它们)并静态链接作为我的程序构建的一部分)。
我希望能够实例化其中一个传递名称,这是一个必须传递给我的程序的参数(作为命令行或作为编译宏)。
我看到唯一可行的解决方案是使用宏:
#ifndef CLASS_NAME
#define CLASS_NAME MyDefaultClassToUse
#endif
BaseClass* o = new CLASS_NAME(param1, param2, ..);
这是唯一有价值的方法吗?
答案 0 :(得分:39)
这是一个通常使用Registry Pattern:
解决的问题这就是这种情况 Registry Pattern描述:
对象需要联系另一个 对象,只知道对象的名称 或者它的服务名称 提供,但不是如何联系它。 提供名称服务 对象,服务或角色 返回一个远程代理 封装了如何使用的知识 联系指定的对象。
它与基本的发布/查找模型相同 这构成了服务的基础 面向架构(SOA)和for 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);
这个宏放在你的头文件中,magic启动它在COM服务器启动时注册单例。
答案 1 :(得分: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.
答案 2 :(得分: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;
}
答案 3 :(得分:2)
在C ++中,必须在编译时做出此决定。
在编译期间,您可以使用typedef而不是macor:
typedef DefaultClass MyDefaultClassToUse;
这是等效的,并避免宏(宏坏; - ))。
如果要在运行时做出决定,您需要编写自己的代码来支持它。 simples解决方案是一个测试字符串并实例化相应类的函数。
它的扩展版本(允许独立代码段注册其类)将是map<name, factory function pointer>
。
答案 4 :(得分:1)
你提到了两种可能性 - 命令行和编译宏,但每种方案的解决方案都大相径庭。
如果选择是由编译宏做出的,那么这是一个简单的问题,可以通过#defines和#ifdefs等来解决。你提出的解决方案与任何解决方案一样好。
但是如果使用命令行参数在运行时进行选择,则需要一些能够接收字符串并创建适当对象的Factory框架。这可以使用一个简单的静态if().. else if()... else if()...
链来完成,该链具有所有可能性,或者可以是一个完全动态的框架,其中对象注册并被克隆以提供自己的新实例。
答案 5 :(得分:1)
虽然这个问题现在存在了四年多,但它仍然有用。因为在编译和链接主代码文件的那一刻,调用新代码是未知的,这是一个非常常见的情况。根本没有提到这个问题的一个解决方案。因此,我喜欢将观众指向一种不是用C ++构建的不同解决方案。 C ++本身没有能力像Java中已知的Class.forName()
或.NET中已知的Activator.CreateInstance(type)
那样行为。由于上述原因,VM无法动态监控JIT代码。但无论如何,低级虚拟机LLVM为您提供了所需的工具和库来读入已编译的库。基本上,您需要执行两个步骤:
clang -emit-llvm -o foo.bc -c foo.c
ParseIRFile()
中的llvm/IRReader/IRReader.h
方法来解析foo.bc
文件以获取相关函数(LLVM本身只知道函数,因为bitcode是CPU的直接抽象操作码并且对Java字节码等更高级的中间表示非常不熟悉。例如,请参阅此article以获取更完整的代码说明。在设置上面描述的这些步骤之后,您也可以从C ++动态调用其他先前未知的函数和方法。
答案 6 :(得分:0)
过去,我已经实现了Factory模式,使得类可以在运行时自行注册,而工厂本身不必具体了解它们。关键是使用一个名为(IIRC)“初始化附件”的非标准编译器功能,其中在每个类的实现文件中声明一个虚拟静态变量(例如bool),并通过调用注册来初始化它例程。
在这个方案中,每个类都必须#include包含其工厂的头,但工厂除了接口类之外什么都不知道。您可以从构建中逐字添加或删除实现类,并在不进行代码更改的情况下重新编译。
问题是只有一些编译器通过初始化支持附件 - IIRC其他人在第一次使用时初始化文件范围变量(与函数本地静态工作相同),这在这里没有帮助,因为从未访问虚拟变量并且工厂地图总是空着。
我感兴趣的编译器(MSVC和GCC)确实支持这一点,所以对我来说这不是问题。你必须自己决定这个解决方案是否适合你。