有没有一种方法可以从包含类名的字符串实例化对象?

161

我有一个文件:Base.h

class Base;
class DerivedA : public Base;
class DerivedB : public Base;

/*etc...*/

还有另一个文件:BaseFactory.h

#include "Base.h"

class BaseFactory
{
public:
  BaseFactory(const string &sClassName){msClassName = sClassName;};

  Base * Create()
  {
    if(msClassName == "DerivedA")
    {
      return new DerivedA();
    }
    else if(msClassName == "DerivedB")
    {
      return new DerivedB();
    }
    else if(/*etc...*/)
    {
      /*etc...*/
    }
  };
private:
  string msClassName;
};

/*etc.*/

有没有办法将这个字符串转换为实际的类型(类),这样BaseFactory就不需要知道所有可能的Derived类,也不需要为每个派生类编写if()语句?我能否从这个字符串中生成一个类?

我认为在C#中可以通过反射来实现这一点。在C++中是否有类似的东西?


这在C++0x和可变参数模板的帮助下部分地是可能的。 - smerlin
12个回答

247

没有,除非你自己进行映射。C++没有机制可以创建在运行时确定类型的对象。不过,你可以使用一个map来完成这个映射:

template<typename T> Base * createInstance() { return new T; }

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

map_type map;
map["DerivedA"] = &createInstance<DerivedA>;
map["DerivedB"] = &createInstance<DerivedB>;

然后你可以做以下操作

return map[some_string]();

获取新实例。另一个想法是让类型自己注册:

// in base.hpp:
template<typename T> Base * createT() { return new T; }

struct BaseFactory {
    typedef std::map<std::string, Base*(*)()> map_type;

    static Base * createInstance(std::string const& s) {
        map_type::iterator it = getMap()->find(s);
        if(it == getMap()->end())
            return 0;
        return it->second();
    }

protected:
    static map_type * getMap() {
        // never delete'ed. (exist until program termination)
        // because we can't guarantee correct destruction order 
        if(!map) { map = new map_type; } 
        return map; 
    }

private:
    static map_type * map;
};

template<typename T>
struct DerivedRegister : BaseFactory { 
    DerivedRegister(std::string const& s) { 
        getMap()->insert(std::make_pair(s, &createT<T>));
    }
};

// in derivedb.hpp
class DerivedB {
    ...;
private:
    static DerivedRegister<DerivedB> reg;
};

// in derivedb.cpp:
DerivedRegister<DerivedB> DerivedB::reg("DerivedB");

您可以决定为注册创建一个宏。

#define REGISTER_DEC_TYPE(NAME) \
    static DerivedRegister<NAME> reg

#define REGISTER_DEF_TYPE(NAME) \
    DerivedRegister<NAME> NAME::reg(#NAME)

我相信有更好的名称可以用来描述这两个概念。在这里,可能有意义的另一件事是使用shared_ptr

如果你有一组没有共同基类的不相关类型,你可以将函数指针的返回类型改为boost::variant<A, B, C, D, ...>。比如,如果你有一个类Foo、Bar和Baz,那么它看起来像这样:

typedef boost::variant<Foo, Bar, Baz> variant_type;
template<typename T> variant_type createInstance() { 
    return variant_type(T()); 
}

typedef std::map<std::string, variant_type (*)()> map_type;
boost::variant 就像一个联合体,它通过查看用于初始化或分配给它的对象来确定存储在其中的类型。请查看其文档here。最后,使用原始函数指针也有点老式。现代C++代码应该与特定的函数/类型解耦。您可能需要查看Boost.Function以寻找更好的方法。然后它会像这样(映射):
typedef std::map<std::string, boost::function<variant_type()> > map_type;

std::functionstd::shared_ptr将在下一个C++版本中提供。


4
喜欢派生类自行注册的想法。这正是我一直在寻找的方法,可以从工厂中删除硬编码的派生类存在知识。 - Gal Goldman
1
最初由somedave在另一个问题中发布,此代码在VS2010上由于make_pair而出现模板歧义错误。要修复此问题,请将make_pair更改为std :: pair <std :: string,Base *(*)()>,这样应该可以解决这些错误。我还遇到了一些链接错误,通过将BaseFactory :: map_type * BaseFactory :: map = new map_type();添加到base.cpp中进行了修复。 - Spencer Rose
9
您如何确保DerivedB::reg被实际初始化?我的理解是,如果在翻译单元derivedb.cpp中没有定义任何函数或对象,则可能根本没有构造它,依据3.6.2。 - musiphil
2
喜欢自注册。但是为了编译,我需要在我的cpp文件中加入BaseFactory::map_type * BaseFactory::map = NULL;。如果没有这个,链接器会抱怨未知符号map。 - Sven
1
不幸的是,这并不起作用。正如musiphil已经指出的那样,如果在翻译单元“derivedb.cpp”中没有定义其函数或实例,则DerivedB :: reg未初始化。这意味着该类在实际实例化之前尚未注册。有人知道解决方法吗? - Tomasito665
显示剩余7条评论

6

没有。我对这个问题的首选解决方案是创建一个字典,将名称映射到创建方法。想要像这样创建的类,然后使用字典向其中注册创建方法。这在GoF Patterns Book中有详细讨论。


8
谁可以指出这是哪个图案,而不仅仅是指向书本? - josaphatv
我认为他指的是注册表模式。 - jiggunjer
3
对于现在正在阅读此答案的人,我认为这个答案是指使用工厂模式,这是一种使用字典来确定实例化哪个类的实现方式。 - Grimeh

5

4
我在另一个SO问题中回答了有关C++工厂的问题。如果您对灵活的工厂感兴趣,请参见这里。我尝试描述一种旧的方法,即使用宏的方式,该方法从ET++中产生,对我非常有效。 ET++是将旧的MacApp移植到C++和X11的项目。在此努力中,Eric Gamma等开始思考设计模式

2

详细解决方案,用于注册对象,并使用字符串名称访问它们。

common.h:

#ifndef COMMON_H_
#define COMMON_H_


#include<iostream>
#include<string>
#include<iomanip>
#include<map>

using namespace std;
class Base{
public:
    Base(){cout <<"Base constructor\n";}
    virtual ~Base(){cout <<"Base destructor\n";}
};
#endif /* COMMON_H_ */

test1.h:

/*
 * test1.h
 *
 *  Created on: 28-Dec-2015
 *      Author: ravi.prasad
 */

#ifndef TEST1_H_
#define TEST1_H_
#include "common.h"

class test1: public Base{
    int m_a;
    int m_b;
public:
    test1(int a=0, int b=0):m_a(a),m_b(b)
    {
        cout <<"test1 constructor m_a="<<m_a<<"m_b="<<m_b<<endl;
    }
    virtual ~test1(){cout <<"test1 destructor\n";}
};



#endif /* TEST1_H_ */

3. test2.h
#ifndef TEST2_H_
#define TEST2_H_
#include "common.h"

class test2: public Base{
    int m_a;
    int m_b;
public:
    test2(int a=0, int b=0):m_a(a),m_b(b)
    {
        cout <<"test1 constructor m_a="<<m_a<<"m_b="<<m_b<<endl;
    }
    virtual ~test2(){cout <<"test2 destructor\n";}
};


#endif /* TEST2_H_ */

main.cpp:

#include "test1.h"
#include "test2.h"

template<typename T> Base * createInstance(int a, int b) { return new T(a,b); }

typedef std::map<std::string, Base* (*)(int,int)> map_type;

map_type mymap;

int main()
{

    mymap["test1"] = &createInstance<test1>;
    mymap["test2"] = &createInstance<test2>;

     /*for (map_type::iterator it=mymap.begin(); it!=mymap.end(); ++it)
        std::cout << it->first << " => " << it->second(10,20) << '\n';*/

    Base *b = mymap["test1"](10,20);
    Base *b2 = mymap["test2"](30,40);

    return 0;
}

编译并运行它(我已经用Eclipse完成了这个过程)

输出:

Base constructor
test1 constructor m_a=10m_b=20
Base constructor
test1 constructor m_a=30m_b=40

2
boost::functional有一个非常灵活的工厂模板:http://www.boost.org/doc/libs/1_54_0/libs/functional/factory/doc/html/index.html 然而,我的偏好是生成包装类来隐藏映射和对象创建机制。我经常遇到的情况是需要将某个基类的不同派生类映射到键上,其中各个派生类都有一个共同的构造函数签名可用。以下是我迄今为止想出的解决方案。
#ifndef GENERIC_FACTORY_HPP_INCLUDED

//BOOST_PP_IS_ITERATING is defined when we are iterating over this header file.
#ifndef BOOST_PP_IS_ITERATING

    //Included headers.
    #include <unordered_map>
    #include <functional>
    #include <boost/preprocessor/iteration/iterate.hpp>
    #include <boost/preprocessor/repetition.hpp>

    //The GENERIC_FACTORY_MAX_ARITY directive controls the number of factory classes which will be generated.
    #ifndef GENERIC_FACTORY_MAX_ARITY
        #define GENERIC_FACTORY_MAX_ARITY 10
    #endif

    //This macro magic generates GENERIC_FACTORY_MAX_ARITY + 1 versions of the GenericFactory class.
    //Each class generated will have a suffix of the number of parameters taken by the derived type constructors.
    #define BOOST_PP_FILENAME_1 "GenericFactory.hpp"
    #define BOOST_PP_ITERATION_LIMITS (0,GENERIC_FACTORY_MAX_ARITY)
    #include BOOST_PP_ITERATE()

    #define GENERIC_FACTORY_HPP_INCLUDED

#else

    #define N BOOST_PP_ITERATION() //This is the Nth iteration of the header file.
    #define GENERIC_FACTORY_APPEND_PLACEHOLDER(z, current, last) BOOST_PP_COMMA() BOOST_PP_CAT(std::placeholders::_, BOOST_PP_ADD(current, 1))

    //This is the class which we are generating multiple times
    template <class KeyType, class BasePointerType BOOST_PP_ENUM_TRAILING_PARAMS(N, typename T)>
    class BOOST_PP_CAT(GenericFactory_, N)
    {
        public:
            typedef BasePointerType result_type;

        public:
            virtual ~BOOST_PP_CAT(GenericFactory_, N)() {}

            //Registers a derived type against a particular key.
            template <class DerivedType>
            void Register(const KeyType& key)
            {
                m_creatorMap[key] = std::bind(&BOOST_PP_CAT(GenericFactory_, N)::CreateImpl<DerivedType>, this BOOST_PP_REPEAT(N, GENERIC_FACTORY_APPEND_PLACEHOLDER, N));
            }

            //Deregisters an existing registration.
            bool Deregister(const KeyType& key)
            {
                return (m_creatorMap.erase(key) == 1);
            }

            //Returns true if the key is registered in this factory, false otherwise.
            bool IsCreatable(const KeyType& key) const
            {
                return (m_creatorMap.count(key) != 0);
            }

            //Creates the derived type associated with key. Throws std::out_of_range if key not found.
            BasePointerType Create(const KeyType& key BOOST_PP_ENUM_TRAILING_BINARY_PARAMS(N,const T,& a)) const
            {
                return m_creatorMap.at(key)(BOOST_PP_ENUM_PARAMS(N,a));
            }

        private:
            //This method performs the creation of the derived type object on the heap.
            template <class DerivedType>
            BasePointerType CreateImpl(BOOST_PP_ENUM_BINARY_PARAMS(N,const T,& a))
            {
                BasePointerType pNewObject(new DerivedType(BOOST_PP_ENUM_PARAMS(N,a)));
                return pNewObject;
            }

        private:
            typedef std::function<BasePointerType (BOOST_PP_ENUM_BINARY_PARAMS(N,const T,& BOOST_PP_INTERCEPT))> CreatorFuncType;
            typedef std::unordered_map<KeyType, CreatorFuncType> CreatorMapType;
            CreatorMapType m_creatorMap;
    };

    #undef N
    #undef GENERIC_FACTORY_APPEND_PLACEHOLDER

#endif // defined(BOOST_PP_IS_ITERATING)
#endif // include guard

我通常不赞成过度使用宏,但在这里我做了个例外。上述代码生成名为GenericFactory_N的类的GENERIC_FACTORY_MAX_ARITY + 1个版本,对于每个N在0到GENERIC_FACTORY_MAX_ARITY之间(包括边界)。
使用生成的类模板很容易。假设您想要一个工厂使用字符串映射来创建BaseClass派生对象。每个派生对象都需要3个整数作为构造函数参数。
#include "GenericFactory.hpp"

typedef GenericFactory_3<std::string, std::shared_ptr<BaseClass>, int, int int> factory_type;

factory_type factory;
factory.Register<DerivedClass1>("DerivedType1");
factory.Register<DerivedClass2>("DerivedType2");
factory.Register<DerivedClass3>("DerivedType3");

factory_type::result_type someNewObject1 = factory.Create("DerivedType2", 1, 2, 3);
factory_type::result_type someNewObject2 = factory.Create("DerivedType1", 4, 5, 6);

GenericFactory_N类的析构函数是虚拟的,以允许以下操作。
class SomeBaseFactory : public GenericFactory_2<int, BaseType*, std::string, bool>
{
    public:
        SomeBaseFactory() : GenericFactory_2()
        {
            Register<SomeDerived1>(1);
            Register<SomeDerived2>(2);
        }
}; 

SomeBaseFactory factory;
SomeBaseFactory::result_type someObject = factory.Create(1, "Hi", true);
delete someObject;

请注意,这是通用工厂生成器宏的一行。
#define BOOST_PP_FILENAME_1 "GenericFactory.hpp"

假设通用工厂头文件的名称为GenericFactory.hpp。

1
Tor Brede Vekterli提供了一个增强扩展,可以提供您所需的功能。目前,它与当前的boost libs略有不便,但在更改其基本命名空间后,我能够使其与1.48_0配合使用。

http://arcticinteractive.com/static/boost/libs/factory/doc/html/factory/factory.html#factory.factory.reference

回答那些质疑为什么反射(reflection)在C++中有用的人们 - 我将其用于UI和引擎之间的交互 - 用户在UI中选择选项,引擎接收UI选择字符串,并生成所需类型的对象。
在这里使用框架(而不是维护某个水果列表)的主要好处是注册函数在每个类的定义中(每个注册类仅需要一行代码调用注册函数)- 而不是包含水果列表的文件,每次派生新类时必须手动添加。
我将工厂设置为基类的静态成员。

0

一个C++11风格的完整示例:

// Base.h
class Base;
class DerivedA : public Base;
class DerivedB : public Base;

// BaseFactory.h
class BaseFactory
{
public:
    static BaseFactory& get() {
        static BaseFactory singleton;
        return singleton;
    }

    virtual ~BaseFactory() {};

    BaseFactory(const BaseFactory&) = delete;
    BaseFactory(BaseFactory&&) = delete;

    template <class DerivedClass>
    static std::shared_ptr<Base> creator()
    {
        return std::shared_ptr<Base>(new DerivedClass());
    }

    template <class DerivedClass>
    void register_class(const std::string& class_name)
    {
        if (name_to_creator_map.find(class_name) == name_to_creator_map.end())
        {
            std::function<std::shared_ptr<Base>(void)> functor = &BaseFactory::template creator<DerivedClass>;
            name_to_creator_map.emplace(class_name, functor);
        }
    }

    std::shared_ptr<Base> create(const std::string& class_name) const;

private:
    BaseFactory();

    std::map<std::string, std::function<std::shared_ptr<Base>(void)>> name_to_creator_map;
};

// example.cpp using BaseFactory
BaseFactory::get().register_class<DerivedA>("DerivedA");
BaseFactory::get().register_class<DerivedB>("DerivedB");
auto a_obj = BaseFactory::get().create("DerivedA");
auto b_obj = BaseFactory::get().create("DerivedB");

0

你所提到的页面上的内容与标准C++相差甚远。 - anon

0

这是工厂模式。请参考维基百科(以及this的例子)。你不能仅通过字符串创建一个类型,除非使用一些极其恶劣的黑客手段。你为什么需要这个?


我需要这个是因为我从文件中读取字符串,如果我有了这个,那么我可以让工厂如此通用,以至于它不需要知道任何信息就能创建正确的实例。这非常强大。 - Gal Goldman
那么,你是说既然汽车和公交车都是车辆,所以不需要为它们分别定义不同的类?但是,如果需要的话,添加另一行代码也不应该成为问题 :) 使用映射方法也会遇到相同的问题--你需要更新映射内容。宏的方法适用于简单的类。 - dirkgently
我想说的是,为了在我的情况下创建一辆公交车或汽车,我不需要不同的定义,否则工厂设计模式将永远不会被使用。我的目标是让工厂尽可能愚蠢。但我在这里看到没有逃脱的机会 :-) - Gal Goldman

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