Czy istnieje sposób na utworzenie instancji obiektów z łańcucha zawierającego nazwę ich klasy?


143

Mam plik: Base.h

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

/*etc...*/

i inny plik: 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.*/

Czy istnieje sposób, aby w jakiś sposób przekonwertować ten ciąg na rzeczywisty typ (klasę), tak aby BaseFactory nie musiał znać wszystkich możliwych klas pochodnych i mieć if () dla każdej z nich? Czy mogę utworzyć klasę z tego ciągu?

Myślę, że można to zrobić w C # za pomocą refleksji. Czy jest coś podobnego w C ++?


jest to częściowo możliwe w C ++ 0x i szablonach
wariadycznych

Odpowiedzi:


227

Nie, nie ma, chyba że sam wykonasz mapowanie. C ++ nie ma mechanizmu do tworzenia obiektów, których typy są określane w czasie wykonywania. Możesz jednak użyć mapy, aby wykonać to mapowanie samodzielnie:

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>;

A potem możesz to zrobić

return map[some_string]();

Zdobycie nowej instancji. Innym pomysłem jest zarejestrowanie się typów:

// 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");

Możesz zdecydować się na utworzenie makra do rejestracji

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

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

Jestem pewien, że dla tych dwóch są lepsze nazwy. Inną rzeczą, która prawdopodobnie ma sens tutaj, jest shared_ptr.

Jeśli masz zestaw niepowiązanych typów, które nie mają wspólnej klasy bazowej, możesz boost::variant<A, B, C, D, ...>zamiast tego nadać wskaźnikowi funkcji zwracany typ . Jak jeśli masz klasę Foo, Bar i Baz, wygląda to tak:

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;

A boost::variantjest jak związek. Wie, jaki typ jest w nim przechowywany, sprawdzając, jaki obiekt został użyty do zainicjowania lub przypisania do niego. Zajrzyj do jego dokumentacji tutaj . Wreszcie użycie surowego wskaźnika funkcji jest również nieco przestarzałe. Nowoczesny kod C ++ powinien być oddzielony od określonych funkcji / typów. Możesz zajrzeć, Boost.Functionaby znaleźć lepszy sposób. Wtedy wyglądałoby to tak (mapa):

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

std::functionbędzie również dostępny w następnej wersji C ++, w tym std::shared_ptr.


3
Podobał mi się pomysł, że klasy pochodne będą się rejestrować. To jest dokładnie to, czego szukałem, sposób na usunięcie zakodowanej na stałe wiedzy o tym, które klasy pochodne istnieją z fabryki.
Gal Goldman,

1
Pierwotnie opublikowany przez somedave w innym pytaniu, ten kod nie działa w VS2010 z niejednoznacznymi błędami szablonu z powodu make_pair. Aby to naprawić, zmień make_pair na std :: pair <std :: string, Base * ( ) ()> i powinno to naprawić te błędy. Otrzymałem również kilka błędów w łączeniu, które zostały naprawione przez dodanie BaseFactory :: map_type BaseFactory :: map = new map_type (); to base.cpp
Spencer Rose

9
Jak upewnić się, że DerivedB::regjest faktycznie zainicjowany? Rozumiem, że nie można go w ogóle skonstruować, jeśli w jednostce tłumaczeniowej nie zdefiniowano żadnej funkcji ani przedmiotu derivedb.cpp, zgodnie z 3.6.2.
musiphil

2
Uwielbiam samorejestrację. Aby skompilować, potrzebowałem BaseFactory::map_type * BaseFactory::map = NULL;w moim pliku cpp. Bez tego linker narzekał na nieznaną mapę symboli.
Sven

1
Niestety to nie działa. Jak już wskazał musiphil, DerivedB::regnie jest inicjowany, jeśli żadna z jego funkcji lub instancji nie jest zdefiniowana w jednostce tłumaczeniowej derivedb.cpp. Oznacza to, że klasa nie jest rejestrowana, dopóki nie zostanie faktycznie utworzona. Czy ktoś zna obejście tego problemu?
Tomasito665

7

Nie, nie ma. Moim preferowanym rozwiązaniem tego problemu jest utworzenie słownika, który odwzorowuje nazwę na sposób tworzenia. Klasy, które chcą zostać utworzone w ten sposób, rejestrują metodę tworzenia w słowniku. Jest to omówione szczegółowo w książce wzorców GoF .


5
Czy ktoś woli zidentyfikować, który to wzór, a nie tylko wskazać książkę?
josaphatv

Myślę, że odnosi się do wzorca rejestru.
jiggunjer

2
Dla tych, którzy czytają teraz tę odpowiedź, sądzę, że odpowiedź odnosi się do użycia wzorca Factory, implementacji, która używa słownika do określenia, której klasy należy utworzyć.
Grimeh


4

Odpowiedziałem w innym pytaniu SO dotyczącym fabryk C ++. Proszę, zobacz tam czy interesuje Cię elastyczna fabryka. Próbuję opisać stary sposób z ET ++ do używania makr, który świetnie się sprawdził.

ET ++ był projektem przeniesienia starego MacApp na C ++ i X11. W tym celu Eric Gamma itp. Zaczął myśleć o wzorcach projektowych


2

boost :: function ma dość elastyczny szablon fabryki: http://www.boost.org/doc/libs/1_54_0/libs/functional/factory/doc/html/index.html

Preferuję jednak generowanie klas opakowujących, które ukrywają mechanizm mapowania i tworzenia obiektów. Typowym scenariuszem, z którym się spotykam, jest potrzeba mapowania różnych klas pochodnych niektórych klas bazowych na klucze, gdzie wszystkie klasy pochodne mają dostępną wspólną sygnaturę konstruktora. Oto rozwiązanie, które do tej pory wymyśliłem.

#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

Generalnie sprzeciwiam się intensywnemu używaniu makr, ale zrobiłem tutaj wyjątek. Powyższy kod generuje wersje GENERIC_FACTORY_MAX_ARITY + 1 klasy o nazwie GenericFactory_N, dla każdego N od 0 do GENERIC_FACTORY_MAX_ARITY włącznie.

Korzystanie z wygenerowanych szablonów klas jest łatwe. Załóżmy, że chcesz, aby fabryka tworzyła obiekty pochodne BaseClass przy użyciu mapowania ciągów. Każdy z obiektów pochodnych przyjmuje 3 liczby całkowite jako parametry konstruktora.

#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);

Destruktor klasy GenericFactory_N jest wirtualny, aby umożliwić następujące działania.

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;

Zwróć uwagę, że ten wiersz ogólnego makra generatora fabrycznego

#define BOOST_PP_FILENAME_1 "GenericFactory.hpp"

Zakłada się, że ogólny plik nagłówkowy fabryki nosi nazwę GenericFactory.hpp


2

Szczegółowe rozwiązanie do rejestracji obiektów i uzyskiwania dostępu do nich za pomocą nazw ciągów.

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;
}

Skompiluj i uruchom (zrobiłem to z Eclipse)

Wynik:

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


1

Tor Brede Vekterli zapewnia rozszerzenie doładowania, które zapewnia dokładnie taką funkcjonalność, jakiej szukasz. Obecnie jest to trochę niewygodne z obecnymi bibliotekami boost, ale udało mi się je uruchomić z 1.48_0 po zmianie jego podstawowej przestrzeni nazw.

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

Odpowiadając tym, którzy pytają, dlaczego coś takiego (jak odbicie) miałoby się przydać w c ++ - używam go do interakcji między UI a silnikiem - użytkownik wybiera opcję w UI, a silnik bierze ciąg wyboru UI, i tworzy obiekt pożądanego typu.

Główną zaletą korzystania z tego frameworka w tym miejscu (zamiast utrzymywania gdzieś listy owoców) jest to, że funkcja rejestrująca znajduje się w definicji każdej klasy (i wymaga tylko jednej linii kodu wywołującej funkcję rejestracji na zarejestrowaną klasę) - w przeciwieństwie do pliku zawierającego lista owoców, którą należy dodać ręcznie za każdym razem, gdy tworzona jest nowa klasa.

Zrobiłem fabrykę statycznym członkiem mojej klasy bazowej.


0

To jest wzór fabryczny. Zobacz wikipedię (i ten przykład). Nie możesz stworzyć typu per se z łańcucha bez rażącego hackowania. Dlaczego tego potrzebujesz?


Potrzebuję tego, ponieważ czytam ciągi z pliku, a jeśli to mam, to mogę mieć fabrykę tak ogólną, że nie musiałaby nic wiedzieć, aby utworzyć właściwą instancję. To jest bardzo potężne.
Gal Goldman,

Więc, czy chcesz powiedzieć, że nie potrzebujesz różnych definicji klas dla autobusu i samochodu, ponieważ oba są pojazdami? Jeśli jednak to zrobisz, dodanie kolejnej linii nie powinno naprawdę stanowić problemu :) Podejście do mapy ma ten sam problem - aktualizujesz zawartość mapy. Rzecz makro działa na trywialnych klasach.
dirkgently

Mówię, że aby STWORZYĆ Autobus lub Samochód w moim przypadku, nie potrzebuję różnych definicji, w przeciwnym razie wzorzec projektowy Factory nigdy nie byłby używany. Moim celem było, aby fabryka była tak głupia, jak to tylko możliwe. Ale widzę tutaj, że nie ma ucieczki :-)
Gal Goldman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.