Есть ли способ создать экземпляры объектов из строки, содержащей имя их класса?


146

У меня есть файл: 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 не знала все возможные производные классы и имела if () для каждого из них? Могу ли я создать класс из этой строки?

Я думаю, что это можно сделать на C # через Reflection. Есть ли что-то подобное в C ++?


частично это возможно с C ++ 0x и вариативными шаблонами ..
smerlin

Ответы:


231

Нет, нет, если вы сами не составите карту. В C ++ нет механизма для создания объектов, типы которых определяются во время выполнения. Однако вы можете использовать карту, чтобы сделать это отображение самостоятельно:

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похоже на союз. Он знает, какой тип хранится в нем, глядя, какой объект использовался для его инициализации или присвоения ему. Взгляните на его документацию здесь . Наконец, использование необработанного указателя на функцию также немного устарело. Современный код C ++ должен быть отделен от определенных функций / типов. Возможно, вы захотите Boost.Functionнайти лучший способ. Тогда это выглядело бы так (карта):

typedef std::map<std::string, boost::function<variant_type()> > map_type;

std::functionбудет доступен и в следующей версии C ++, в том числе std::shared_ptr.


3
Очень понравилась идея, что производные классы будут регистрироваться. Это именно то, что я искал, способ избавиться от жестко запрограммированного знания о том, какие производные классы существуют из фабрики.
Гэл Голдман,

1
Первоначально опубликованный somedave в другом вопросе, этот код не работает на VS2010 с неоднозначными ошибками шаблона из-за make_pair. Чтобы исправить это, измените make_pair на std :: pair <std :: string, Base * ( ) ()>, и он должен исправить эти ошибки. У меня также были ошибки связывания, которые были исправлены добавлением BaseFactory :: map_type BaseFactory :: map = new map_type (); на base.cpp
Спенсер Роуз

9
Как убедиться, что DerivedB::regон действительно инициализирован? Насколько я понимаю, он может вообще не быть построен, если в модуле перевода не определена функция или объект, как указано в derivedb.cpp3.6.2.
musiphil

2
Люблю самостоятельную регистрацию. Для компиляции мне нужен был BaseFactory::map_type * BaseFactory::map = NULL;в моем файле cpp. Без этого компоновщик жаловался на неизвестную карту символов.
Sven

1
К сожалению, это не работает. Как уже указывалось musiphil, DerivedB::regне инициализируется, если ни одна из его функций или экземпляров не определена в блоке перевода derivedb.cpp. Это означает, что класс не регистрируется до тех пор, пока не будет активирован. Кто-нибудь знает обходной путь для этого?
Tomasito665

7

Нет, нет. Мое предпочтительное решение этой проблемы - создать словарь, который сопоставляет имя с методом создания. Классы, которые хотят быть созданы таким образом, затем регистрируют метод создания в словаре. Это подробно обсуждается в книге шаблонов GoF .


5
Кто-нибудь хочет определить, что это за образец, а не просто указывать на книгу?
josaphatv

Я думаю, он имеет в виду шаблон реестра.
jiggunjer

2
Для тех, кто сейчас читает этот ответ, я считаю, что ответ относится к использованию шаблона Factory, реализации, которая использует словарь для определения, какой класс создать.
Grimeh


4

Я ответил еще на один вопрос SO о фабриках C ++. Пожалуйста, посмотрите там, если вас интересует гибкая фабрика. Я пытаюсь описать старый способ использования макросов из ET ++, который мне очень понравился.

ET ++ был проектом по переносу старого MacApp на C ++ и X11. Эрик Гамма и другие начали думать о паттернах дизайна.


2

boost :: function имеет довольно гибкий фабричный шаблон: 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

Я вообще против интенсивного использования макросов, но здесь я сделал исключение. Приведенный выше код генерирует GENERIC_FACTORY_MAX_ARITY + 1 версии класса с именем GenericFactory_N для каждого 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.


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


1

Tor Brede Vekterli предоставляет расширение для ускорения, которое дает именно ту функциональность, которую вы ищете. В настоящее время он немного неудобно подходит для текущих библиотек boost, но мне удалось заставить его работать с 1.48_0 после изменения его базового пространства имен.

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

Отвечая тем, кто задается вопросом, почему такая вещь (как отражение) может быть полезна для С ++ - я использую ее для взаимодействия между пользовательским интерфейсом и движком - пользователь выбирает параметр в пользовательском интерфейсе, а движок принимает строку выбора пользовательского интерфейса, и создает объект желаемого типа.

Основное преимущество использования фреймворка здесь (по сравнению с сохранением где-то списка фруктов) заключается в том, что функция регистрации присутствует в определении каждого класса (и требует только одной строки кода, вызывающей функцию регистрации для каждого зарегистрированного класса) - в отличие от файла, содержащего список фруктов, который необходимо добавлять вручную каждый раз, когда создается новый класс.

Я сделал фабрику статическим членом моего базового класса.


0

Это заводской узор. См. Википедию (и этот пример). Вы не можете создать тип как таковой из строки без какого-либо вопиющего взлома. зачем вам это?


Мне это нужно, потому что я читаю строки из файла, и если он у меня есть, тогда у меня может быть фабрика настолько универсальная, что ей не нужно ничего знать, чтобы создать правильный экземпляр. Это очень мощно.
Гэл Голдман,

Итак, вы говорите, что вам не понадобятся разные определения классов для автобуса и автомобиля, поскольку они оба являются транспортными средствами? Однако, если вы это сделаете, добавление еще одной строки не должно быть проблемой :) Подход с картой имеет ту же проблему - вы обновляете содержимое карты. Макросъемка работает для тривиальных классов.
dirkgently

Я говорю, что для СОЗДАНИЯ автобуса или автомобиля в моем случае мне не нужны другие определения, иначе шаблон проектирования Factory никогда бы не использовался. Моей целью было сделать фабрику настолько тупой, насколько это возможно. Но я вижу здесь, что выхода нет :-)
Гэл Голдман
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.