Чи є спосіб інстанціювати об'єкти з рядка, що містить ім'я їх класу?


143

У мене є файл: 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

Відповіді:


227

Ні, немає, якщо ви не зробите картування самостійно. 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 (); to base.cpp
Spencer Rose

9
Як ви гарантуєте, що DerivedB::regнасправді ініціалізується? Моє розуміння полягає в тому, що він може взагалі не будуватися, якщо в блоці перекладу не визначено жодної функції чи об'єкта derivedb.cpp, як передбачено пунктом 3.6.2.
musiphil

2
Любіть самореєстрацію. Для компіляції, хоча мені знадобився BaseFactory::map_type * BaseFactory::map = NULL;в моєму файлі cpp. Без цього лінкер скаржився на невідому карту символів.
Свен

1
На жаль, це не працює. Як уже зазначалося musiphil, DerivedB::regвін не ініціалізується, якщо жодна з його функцій або примірників не визначена в блоці перекладу derivedb.cpp. Це означає, що клас не реєструється до тих пір, поки він фактично миттєвий. Хтось знає рішення для цього?
Tomasito665

7

Ні, немає. Моїм кращим рішенням цієї проблеми є створення словника, який відображає ім'я до методу створення. Класи, які хочуть створити так, реєструють метод створення зі словником. Про це детально йде мова в книзі з моделями GoF .


5
Комусь важливо визначити, яка це закономірність, а не просто вказати на книгу?
Йосафатв

Я думаю, що він має на увазі схему реєстру.
jiggunjer

2
Для тих, хто зараз читає цю відповідь, я вважаю, що відповідь стосується використання заводського шаблону - реалізації, яка використовує словник для визначення, який клас інстанціювати.
Grimeh


4

Я відповів на ще одне питання щодо фабрик С ++. Будь ласка, подивіться там, якщо цікава гнучка фабрика. Я намагаюся описати старий шлях від ET ++, щоб використовувати макроси, які чудово працювали для мене.

ET ++ був проектом для перенесення старого MacApp на C ++ та X11. У зусиллях Ерік Гамма тощо почав думати про дизайнерські шаблони


2

boost :: funkcional має досить гнучкий заводський шаблон: 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 забезпечує розширення, що забезпечує саме ті функціональні можливості, які ви прагнете. Наразі це трохи незручно підходить до поточних прискорених ліфтів, але мені вдалося змусити його працювати з 1,48_0 після зміни базового простору імен.

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

Відповідаючи на запитання, чому така річ (як відображення) була б корисною для c ++ - я використовую її для взаємодії між інтерфейсом користувача та двигуном - користувач вибирає варіант у інтерфейсі, а двигун бере рядок вибору інтерфейсу, і виробляє об’єкт потрібного типу.

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

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


0

Це заводський зразок. Дивіться вікіпедію (і цей приклад). Ви не можете створити тип сам по собі з рядка без явного злому. Навіщо вам це потрібно?


Мені це потрібно, тому що я читаю рядки з файлу, і якщо у мене є, то я можу мати фабрику настільки загальною, що їй не доведеться нічого знати, щоб створити потрібний екземпляр. Це дуже потужно.
Гал Голдман

Отже, ви говорите, що вам не знадобляться різні визначення класу для Автобусу та Автомобіля, оскільки вони обидва Транспортні засоби? Однак, якщо це зробити, додавання іншого рядка насправді не повинно бути проблемою :) У підході до карти є та сама проблема - ви оновлюєте вміст карти. Макро-штука працює для тривіальних класів.
несподівано

Я кажу, що для того, щоб СТВОРИТИ Автобус чи Автомобіль в моєму випадку, мені не потрібні різні визначення, інакше модель дизайну Фабрики ніколи не буде використана. Моя мета полягала в тому, щоб фабрика була якомога дурнішою. Але я бачу тут, що виходу немає :-)
Гал Голдман,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.