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


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.