Existe uma maneira de instanciar objetos a partir de uma string contendo o nome da classe?

143

Eu tenho um arquivo: Base.h

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

/*etc...*/

e outro arquivo: 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 uma maneira de converter de alguma forma essa seqüência de caracteres em um tipo real (classe), para que BaseFactory não precise conhecer todas as classes Derived possíveis e tenha if () para cada uma delas? Posso produzir uma classe dessa string?

Eu acho que isso pode ser feito em c # através de reflexão. Existe algo semelhante em C ++?

Gal Goldman
fonte
sua parcialmente possível com C ++ 0x e modelos variádicos ..
smerlin

Respostas:

227

Não, não há, a menos que você mesmo faça o mapeamento. O C ++ não tem mecanismo para criar objetos cujos tipos são determinados em tempo de execução. Você pode usar um mapa para fazer esse mapeamento, no entanto:

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

E então você pode fazer

return map[some_string]();

Obtendo uma nova instância. Outra idéia é fazer com que os tipos se registrem:

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

Você pode optar por criar uma macro para o registro

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

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

Tenho certeza de que existem nomes melhores para esses dois. Outra coisa que provavelmente faz sentido usar aqui é shared_ptr.

Se você tiver um conjunto de tipos não relacionados que não possuem classe base comum, poderá fornecer ao ponteiro da função um tipo de retorno boost::variant<A, B, C, D, ...>. Por exemplo, se você tem uma classe Foo, Bar e Baz, fica assim:

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::varianté como uma união. Ele sabe qual tipo é armazenado nele, procurando qual objeto foi usado para inicializar ou atribuir a ele. Dê uma olhada na documentação aqui . Finalmente, o uso de um ponteiro de função bruta também é um pouco antigo. O código C ++ moderno deve ser dissociado de funções / tipos específicos. Você pode Boost.Functionprocurar uma maneira melhor. Ficaria assim então (o mapa):

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

std::functiontambém estará disponível na próxima versão do C ++, inclusive std::shared_ptr.

Johannes Schaub - litb
fonte
3
Adorei a ideia de que as classes derivadas se registrariam. É exatamente o que eu estava procurando, uma maneira de remover o conhecimento codificado de quais classes derivadas existem da fábrica.
Gal Goldman
1
Originalmente publicado por somedave em outra pergunta, esse código falha no VS2010 com erros de modelo ambíguos devido ao make_pair. Para corrigir, altere make_pair para std :: pair <std :: string, Base * ( ) ()> e deve corrigir esses erros. Também obtive alguns erros de vinculação que foram corrigidos adicionando BaseFactory :: map_type BaseFactory :: map = new map_type (); Para base.cpp
Spencer Rose
9
Como você garante que DerivedB::regseja realmente inicializado? Meu entendimento é que ele pode não ser construído se nenhuma função ou objeto definido na unidade de tradução derivedb.cpp, conforme 3.6.2.
Musiphil 8/12/12
2
Ame o auto-registro. Para compilar, eu precisava de um BaseFactory::map_type * BaseFactory::map = NULL;no meu arquivo cpp. Sem isso, o vinculador reclamou do mapa de símbolos desconhecido.
Sven
1
Infelizmente, isso não funciona. Como o musiphil já apontou, DerivedB::regnão será inicializado se nenhuma de suas funções ou instâncias estiver definida na unidade de tradução derivedb.cpp. Isso significa que a classe não é registrada até que seja realmente instanciada. Alguém conhece uma solução alternativa para isso?
Tomasito665
7

Não, não existe. Minha solução preferida para esse problema é criar um dicionário que mapeie o nome para o método de criação. Classes que desejam ser criadas assim registram um método de criação no dicionário. Isso é discutido em alguns detalhes no livro de padrões do GoF .


fonte
5
Alguém se importa em identificar qual é esse padrão, em vez de apenas apontar para o livro?
Josaphatv 29/05
Eu acho que ele está se referindo ao padrão de registro.
jiggunjer
2
Para aqueles que estão lendo esta resposta agora, acredito que a resposta esteja se referindo ao uso do padrão Factory, uma implementação que usa um dicionário para determinar qual classe instanciar.
Grimeh
4

Eu respondi em outra pergunta SO sobre fábricas C ++. Veja se uma fábrica flexível é de seu interesse. Eu tento descrever uma maneira antiga do ET ++ para usar macros que funcionou muito bem para mim.

O ET ++ foi um projeto para portar o MacApp antigo para C ++ e X11. No esforço disso, Eric Gamma etc começou a pensar em Design Patterns

epatel
fonte
2

O boost :: functional possui um modelo de fábrica bastante flexível: http://www.boost.org/doc/libs/1_54_0/libs/functional/factory/doc/html/index.html

Minha preferência, porém, é gerar classes de wrapper que ocultam o mapeamento e o mecanismo de criação de objetos. O cenário comum que encontro é a necessidade de mapear diferentes classes derivadas de alguma classe base para chaves, onde todas as classes derivadas têm uma assinatura de construtor comum disponível. Aqui está a solução que eu encontrei até agora.

#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

Geralmente sou contra o uso intenso de macro, mas fiz uma exceção aqui. O código acima gera GENERIC_FACTORY_MAX_ARITY + 1 versões de uma classe chamada GenericFactory_N, para cada N entre 0 e GENERIC_FACTORY_MAX_ARITY, inclusive.

Usar os modelos de classe gerados é fácil. Suponha que você queira que uma fábrica crie objetos derivados de BaseClass usando um mapeamento de string. Cada um dos objetos derivados aceita 3 números inteiros como parâmetros do construtor.

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

O destruidor de classe GenericFactory_N é virtual para permitir o seguinte.

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;

Observe que esta linha da macro do gerador de fábrica genérica

#define BOOST_PP_FILENAME_1 "GenericFactory.hpp"

Assume que o arquivo de cabeçalho de fábrica genérico se chama GenericFactory.hpp

texta83
fonte
2

Solução detalhada para registrar os objetos e acessá-los com nomes de cadeias.

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

Compile e execute (já fez isso com o Eclipse)

Resultado:

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

Tor Brede Vekterli fornece uma extensão de impulso que fornece exatamente a funcionalidade que você procura. Atualmente, é um pouco estranho para as bibliotecas de impulso atuais, mas consegui fazê-lo funcionar com 1.48_0 depois de alterar seu namespace base.

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

Em resposta a quem pergunta por que algo (como reflexão) seria útil para c ++ - eu o uso para interações entre a interface do usuário e um mecanismo - o usuário seleciona uma opção na interface do usuário e o mecanismo pega a sequência de seleção da interface do usuário, e produz um objeto do tipo desejado.

O principal benefício de usar a estrutura aqui (manter uma lista de frutas em algum lugar) é que a função de registro está na definição de cada classe (e requer apenas uma linha de código que chama a função de registro por classe registrada) - em oposição a um arquivo que contém a lista de frutas, que deve ser adicionada manualmente a cada vez que uma nova classe for derivada.

Eu fiz da fábrica um membro estático da minha classe base.

DAmann
fonte
0

Esse é o padrão de fábrica. Veja a Wikipedia (e este exemplo). Você não pode criar um tipo per se a partir de uma cadeia de caracteres sem algum truque flagrante. Por que você precisa disso?

dirkgently
fonte
Preciso disso porque leio as strings de um arquivo e, se eu tiver, posso ter a fábrica tão genérica que não precisaria saber nada para criar a instância correta. Isso é muito poderoso.
Gal Goldman
Então, você está dizendo que não precisará de definições de classe diferentes para um ônibus e um carro, pois ambos são veículos? No entanto, se o fizer, adicionar outra linha não deve ser realmente um problema :) A abordagem do mapa tem o mesmo problema - você atualiza o conteúdo do mapa. A coisa macro funciona para classes triviais.
dirkgently
Estou dizendo que, para criar um ônibus ou um carro no meu caso, não preciso de definições diferentes; caso contrário, o padrão de design da fábrica nunca estaria em uso. Meu objetivo era ter a fábrica o mais estúpida possível. Mas eu vejo aqui que não há escapatória :-)
Gal Goldman