Existe-t-il un moyen d'instancier des objets à partir d'une chaîne contenant leur nom de classe?

143

J'ai un fichier: Base.h

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

/*etc...*/

et un autre fichier: 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.*/

Existe-t-il un moyen de convertir en quelque sorte cette chaîne en un type réel (classe), de sorte que BaseFactory n'ait pas à connaître toutes les classes dérivées possibles et à avoir if () pour chacune d'elles? Puis-je produire une classe à partir de cette chaîne?

Je pense que cela peut être fait en C # via Reflection. Y a-t-il quelque chose de similaire en C ++?

Gal Goldman
la source
c'est partiellement possible avec C ++ 0x et des modèles variadiques ..
smerlin

Réponses:

227

Non, il n'y en a pas, à moins que vous ne fassiez la cartographie vous-même. C ++ n'a aucun mécanisme pour créer des objets dont les types sont déterminés au moment de l'exécution. Vous pouvez cependant utiliser une carte pour faire ce mappage vous-même:

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

Et puis tu peux faire

return map[some_string]();

Obtenir une nouvelle instance. Une autre idée est de faire enregistrer les types eux-mêmes:

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

Vous pouvez décider de créer une macro pour l'enregistrement

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

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

Je suis sûr qu'il y a de meilleurs noms pour ces deux là. Une autre chose qui a probablement du sens à utiliser ici est shared_ptr.

Si vous avez un ensemble de types non liés qui n'ont pas de classe de base commune, vous pouvez attribuer au pointeur de fonction un type de retour à la boost::variant<A, B, C, D, ...>place. Comme si vous avez une classe Foo, Bar et Baz, cela ressemble à ceci:

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::variantest comme une union. Il sait quel type y est stocké en recherchant quel objet a été utilisé pour l'initialiser ou l'assigner. Jetez un œil à sa documentation ici . Enfin, l'utilisation d'un pointeur de fonction brute est également un peu ancienne. Le code C ++ moderne doit être découplé des fonctions / types spécifiques. Vous voudrez peut-être examiner Boost.Functionpour trouver une meilleure façon. Cela ressemblerait à ceci alors (la carte):

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

std::functionsera également disponible dans la prochaine version de C ++, y compris std::shared_ptr.

Johannes Schaub - litb
la source
3
J'ai adoré l'idée que les classes dérivées s'enregistreront elles-mêmes. C'est exactement ce que je recherchais, un moyen de supprimer la connaissance codée en dur des classes dérivées existantes de l'usine.
Gal Goldman
1
Publié à l'origine par somedave dans une autre question, ce code échoue sur VS2010 avec des erreurs de modèle ambiguës à cause de make_pair. Pour corriger, changez make_pair en std :: pair <std :: string, Base * ( ) ()> et cela devrait corriger ces erreurs. J'ai également eu des erreurs de liaison qui ont été corrigées en ajoutant BaseFactory :: map_type BaseFactory :: map = new map_type (); à base.cpp
Spencer Rose
9
Comment vous assurez-vous qu'il DerivedB::regest réellement initialisé? Je crois comprendre qu'il peut ne pas être construit du tout si aucune fonction ou aucun objet n'est défini dans l'unité de traduction derivedb.cpp, conformément à 3.6.2.
musiphil
2
J'adore l'auto-inscription. Pour compiler, j'avais besoin d'un BaseFactory::map_type * BaseFactory::map = NULL;dans mon fichier cpp. Sans cela, l'éditeur de liens s'est plaint de la carte des symboles inconnus.
Sven
1
Malheureusement, cela ne fonctionne pas. Comme musiphil l'a déjà souligné, DerivedB::regn'est pas initialisé si aucune de ses fonctions ou instances n'est définie dans l'unité de traduction derivedb.cpp. Cela signifie que la classe n'est pas enregistrée tant qu'elle n'est pas réellement instanciée. Quelqu'un connaît-il une solution de contournement pour cela?
Tomasito665
7

Non, il n'y en a pas. Ma solution préférée à ce problème est de créer un dictionnaire qui mappe le nom à la méthode de création. Les classes qui veulent être créées de cette manière enregistrent ensuite une méthode de création avec le dictionnaire. Ceci est discuté en détail dans le livre des modèles du GoF .


la source
5
Quelqu'un veut-il identifier de quel modèle il s'agit, plutôt que de simplement pointer le livre?
josaphatv
Je pense qu'il fait référence au modèle de registre.
jiggunjer
2
Pour ceux qui lisent cette réponse maintenant, je pense que la réponse fait référence à l'utilisation du modèle Factory, une implémentation qui utilise un dictionnaire pour déterminer la classe à instancier.
Grimeh
4

J'ai répondu à une autre question SO sur les usines C ++. Veuillez y voir si une usine flexible présente un intérêt. J'essaie de décrire une ancienne méthode d'ET ++ pour utiliser des macros qui a très bien fonctionné pour moi.

ET ++ était un projet de portage de l'ancien MacApp vers C ++ et X11. Dans cet effort, Eric Gamma, etc. a commencé à penser aux modèles de conception

Epatel
la source
2

boost :: Functional a un template d'usine assez flexible: http://www.boost.org/doc/libs/1_54_0/libs/functional/factory/doc/html/index.html

Ma préférence est cependant de générer des classes wrapper qui masquent le mécanisme de mappage et de création d'objets. Le scénario courant que je rencontre est la nécessité de mapper différentes classes dérivées d'une classe de base vers des clés, où les classes dérivées ont toutes une signature de constructeur commune disponible. Voici la solution que j'ai trouvée jusqu'à présent.

#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

Je suis généralement opposé à une utilisation intensive des macros, mais j'ai fait une exception ici. Le code ci-dessus génère des versions GENERIC_FACTORY_MAX_ARITY + 1 d'une classe nommée GenericFactory_N, pour chaque N compris entre 0 et GENERIC_FACTORY_MAX_ARITY inclus.

L'utilisation des modèles de classe générés est facile. Supposons que vous souhaitiez qu'une fabrique crée des objets dérivés BaseClass à l'aide d'un mappage de chaîne. Chacun des objets dérivés prend 3 entiers comme paramètres de constructeur.

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

Le destructeur de classe GenericFactory_N est virtuel pour permettre ce qui suit.

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;

Notez que cette ligne de la macro générique du générateur d'usine

#define BOOST_PP_FILENAME_1 "GenericFactory.hpp"

Suppose que le fichier d'en-tête de fabrique générique est nommé GenericFactory.hpp

texta83
la source
2

Solution détaillée pour enregistrer les objets et y accéder avec des noms de chaîne.

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

Compilez et exécutez-le (l'avez fait avec Eclipse)

Production:

Base constructor
test1 constructor m_a=10m_b=20
Base constructor
test1 constructor m_a=30m_b=40
user3458845
la source
1

Tor Brede Vekterli fournit une extension boost qui donne exactement la fonctionnalité que vous recherchez. Actuellement, il convient légèrement aux bibliothèques boost actuelles, mais j'ai pu le faire fonctionner avec 1.48_0 après avoir changé son espace de noms de base.

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

En réponse à ceux qui se demandent pourquoi une telle chose (comme réflexion) serait utile pour c ++ - je l'utilise pour les interactions entre l'interface utilisateur et un moteur - l'utilisateur sélectionne une option dans l'interface utilisateur, et le moteur prend la chaîne de sélection de l'interface utilisateur, et produit un objet du type souhaité.

Le principal avantage de l'utilisation du cadre ici (par rapport au maintien d'une liste de fruits quelque part) est que la fonction d'enregistrement est dans la définition de chaque classe (et ne nécessite qu'une seule ligne de code appelant la fonction d'enregistrement par classe enregistrée) - par opposition à un fichier contenant la liste de fruits, qui doit être ajoutée manuellement à chaque fois qu'une nouvelle classe est dérivée.

J'ai fait de l'usine un membre statique de ma classe de base.

DAmann
la source
0

C'est le modèle d'usine. Voir wikipedia (et cet exemple). Vous ne pouvez pas créer un type en soi à partir d'une chaîne sans un hack flagrant. Pourquoi en avez-vous besoin?

dirkgently
la source
J'en ai besoin parce que j'ai lu les chaînes d'un fichier, et si je l'ai, alors je peux avoir l'usine si générique, qu'elle n'aurait pas besoin de savoir quoi que ce soit pour créer la bonne instance. C'est très puissant.
Gal Goldman
Alors, êtes-vous en train de dire que vous n'aurez pas besoin de définitions de classe différentes pour un bus et une voiture puisqu'ils sont tous deux des véhicules? Cependant, si vous le faites, l'ajout d'une autre ligne ne devrait pas vraiment être un problème :) L'approche de la carte a le même problème - vous mettez à jour le contenu de la carte. Le truc macro fonctionne pour les classes triviales.
dirkgently
Je dis que pour CRÉER un bus ou une voiture dans mon cas, je n'ai pas besoin de définitions différentes, sinon le modèle de conception Factory ne serait jamais utilisé. Mon objectif était de rendre l'usine aussi stupide que possible. Mais je vois ici qu'il n'y a pas d'échappatoire :-)
Gal Goldman