Apakah ada cara untuk membuat instance objek dari string yang memegang nama kelas mereka?

143

Saya punya file: Base.h

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

/*etc...*/

dan file lain: 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.*/

Apakah ada cara untuk mengubah string ini menjadi tipe aktual (kelas), sehingga BaseFactory tidak perlu mengetahui semua kelas Derived yang mungkin, dan memiliki if () untuk masing-masing dari mereka? Bisakah saya menghasilkan kelas dari string ini?

Saya pikir ini bisa dilakukan dalam C # melalui Refleksi. Apakah ada yang serupa di C ++?

Gal Goldman
sumber
ini sebagian mungkin dengan C ++ 0x dan templat variadic ..
smerlin

Jawaban:

227

Tidak, tidak ada, kecuali Anda melakukan pemetaan sendiri. C ++ tidak memiliki mekanisme untuk membuat objek yang tipenya ditentukan saat runtime. Anda dapat menggunakan peta untuk melakukan pemetaan sendiri, meskipun:

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

Dan kemudian Anda bisa melakukannya

return map[some_string]();

Mendapatkan contoh baru. Gagasan lain adalah meminta jenis mendaftarkan diri:

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

Anda dapat memutuskan untuk membuat makro untuk pendaftaran

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

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

Saya yakin ada nama yang lebih baik untuk keduanya. Hal lain yang mungkin masuk akal untuk digunakan di sini adalah shared_ptr.

Jika Anda memiliki satu set tipe yang tidak terkait yang tidak memiliki kelas dasar yang sama, Anda dapat memberikan pointer fungsi tipe pengembalian boost::variant<A, B, C, D, ...>sebagai gantinya. Seperti jika Anda memiliki kelas Foo, Bar dan Baz, tampilannya seperti ini:

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::variantseperti serikat pekerja. Ia tahu tipe mana yang disimpan di dalamnya dengan melihat objek apa yang digunakan untuk menginisialisasi atau menugaskannya. Lihat dokumentasinya di sini . Akhirnya, penggunaan pointer fungsi mentah juga agak oldish. Kode C ++ modern harus dipisahkan dari fungsi / tipe tertentu. Anda mungkin ingin melihat ke dalam Boost.Functionuntuk mencari cara yang lebih baik. Maka akan terlihat seperti ini (peta):

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

std::functionakan tersedia di versi C ++ selanjutnya, termasuk std::shared_ptr.

Johannes Schaub - litb
sumber
3
Menyukai gagasan bahwa kelas turunan akan mendaftar sendiri. Persis seperti yang saya cari, sebuah cara untuk menghapus pengetahuan hard-coded dari mana kelas turunan ada dari pabrik.
Gal Goldman
1
Awalnya diposting oleh seseorang di pertanyaan lain, kode ini gagal pada VS2010 dengan kesalahan templat ambigu karena make_pair. Untuk memperbaikinya, ubah make_pair ke std :: pair <std :: string, Base * ( ) ()> dan itu harus memperbaiki kesalahan tersebut. Saya juga mendapatkan beberapa kesalahan penautan yang diperbaiki dengan menambahkan BaseFactory :: map_type BaseFactory :: map = new map_type (); ke base.cpp
Spencer Rose
9
Bagaimana Anda memastikan bahwa DerivedB::regsebenarnya diinisialisasi? Pemahaman saya adalah bahwa itu tidak dapat dibangun sama sekali jika tidak ada fungsi atau objek yang didefinisikan dalam unit terjemahan derivedb.cpp, sesuai 3.6.2.
musiphil
2
Suka pendaftaran diri. Untuk mengkompilasi saya membutuhkan BaseFactory::map_type * BaseFactory::map = NULL;di dalam file cpp saya. Tanpa ini, linker mengeluh tentang peta simbol yang tidak dikenal.
Sven
1
Sayangnya, ini tidak berhasil. Seperti yang telah ditunjukkan oleh musiphil, DerivedB::regtidak diinisialisasi jika tidak ada fungsi atau instansnya yang didefinisikan dalam unit terjemahan derivedb.cpp. Itu berarti bahwa kelas tidak terdaftar sampai itu benar-benar instan. Adakah yang tahu solusi untuk itu?
Tomasito665
7

Tidak ada. Solusi pilihan saya untuk masalah ini adalah membuat kamus yang memetakan nama untuk metode pembuatan. Kelas-kelas yang ingin dibuat seperti ini kemudian mendaftarkan metode pembuatan dengan kamus. Ini dibahas secara rinci dalam buku pola GoF .


sumber
5
Adakah yang mau mengidentifikasi pola apa ini, dan bukan hanya menunjuk pada buku itu?
josaphatv
Saya pikir dia mengacu pada pola registri.
jiggunjer
2
Bagi mereka yang membaca jawaban ini sekarang, saya yakin jawabannya mengacu pada menggunakan pola Pabrik, sebuah implementasi yang menggunakan kamus untuk menentukan kelas mana yang akan dipakai.
Grimeh
4

Saya telah menjawab pertanyaan SO lain tentang pabrik C ++. Silakan lihat di sana jika pabrik yang fleksibel menarik. Saya mencoba menggambarkan cara lama dari ET ++ untuk menggunakan macro yang telah bekerja sangat baik untuk saya.

ET ++ adalah proyek untuk port MacApp lama ke C ++ dan X11. Dalam upaya itu Eric Gamma dll mulai berpikir tentang Pola Desain

epatel
sumber
2

boost :: functional memiliki templat pabrik yang cukup fleksibel: http://www.boost.org/doc/libs/1_54_0/libs/functional/factory/doc/html/index.html

Namun preferensi saya adalah untuk menghasilkan kelas pembungkus yang menyembunyikan mekanisme pemetaan dan pembuatan objek. Skenario umum yang saya temui adalah kebutuhan untuk memetakan kelas turunan berbeda dari beberapa kelas dasar ke kunci, di mana kelas turunan semua memiliki tanda tangan konstruktor umum yang tersedia. Inilah solusi yang saya buat sejauh ini.

#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

Saya umumnya menentang penggunaan makro yang berat, tapi saya membuat pengecualian di sini. Kode di atas menghasilkan GENERIC_FACTORY_MAX_ARITY + 1 versi kelas bernama GenericFactory_N, untuk setiap N antara 0 dan GENERIC_FACTORY_MAX_ARITY inklusif.

Menggunakan templat kelas yang dihasilkan mudah. Misalkan Anda ingin pabrik membuat objek turunan BaseClass menggunakan pemetaan string. Setiap objek yang diturunkan mengambil 3 bilangan bulat sebagai parameter konstruktor.

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

Destructor kelas GenericFactory_N adalah virtual untuk memungkinkan yang berikut ini.

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;

Perhatikan bahwa baris ini makro generator pabrik generik

#define BOOST_PP_FILENAME_1 "GenericFactory.hpp"

Mengasumsikan file header pabrik generik bernama GenericFactory.hpp

texta83
sumber
2

Solusi detail untuk mendaftarkan objek, dan mengaksesnya dengan nama string.

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

Kompilasi dan Jalankan (Telah melakukan ini dengan Eclipse)

Keluaran:

Base constructor
test1 constructor m_a=10m_b=20
Base constructor
test1 constructor m_a=30m_b=40
pengguna3458845
sumber
1

Tor Brede Vekterli menyediakan ekstensi boost yang memberikan fungsionalitas yang Anda cari. Saat ini, itu agak canggung dengan lib boost saat ini, tapi saya bisa membuatnya bekerja dengan 1.48_0 setelah mengubah namespace dasarnya.

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

Sebagai jawaban bagi mereka yang mempertanyakan mengapa hal seperti itu (sebagai refleksi) akan berguna untuk c ++ - Saya menggunakannya untuk interaksi antara UI dan mesin - pengguna memilih opsi di UI, dan mesin mengambil string pemilihan UI, dan menghasilkan objek dari tipe yang diinginkan.

Manfaat utama menggunakan kerangka kerja di sini (lebih dari mempertahankan daftar buah di suatu tempat) adalah bahwa fungsi pendaftaran ada dalam definisi masing-masing kelas (dan hanya memerlukan satu baris kode yang memanggil fungsi pendaftaran per kelas terdaftar) - sebagai lawan dari file yang mengandung daftar buah, yang harus ditambahkan secara manual ke setiap kali kelas baru diturunkan.

Saya menjadikan pabrik sebagai anggota statis kelas dasar saya.

DAmann
sumber
0

Ini adalah pola pabrik. Lihat wikipedia (dan contoh ini ). Anda tidak dapat membuat tipe per se dari string tanpa beberapa hack mengerikan. Mengapa Anda membutuhkan ini?

secara langsung
sumber
Saya memerlukan ini karena saya membaca string dari file, dan jika saya memiliki ini, maka saya dapat memiliki pabrik yang sangat umum, sehingga tidak perlu tahu apa pun untuk membuat contoh yang tepat. Ini sangat kuat.
Gal Goldman
Jadi, apakah Anda mengatakan Anda tidak akan memerlukan definisi kelas yang berbeda untuk Bus dan Mobil karena keduanya adalah Kendaraan? Namun, jika Anda melakukannya, menambahkan baris lain seharusnya tidak benar-benar menjadi masalah :) Pendekatan peta memiliki masalah yang sama - Anda memperbarui konten peta. Makro makro berfungsi untuk kelas sepele.
dirkgently
Saya mengatakan bahwa untuk MENCIPTAKAN Bus atau Mobil dalam kasus saya, saya tidak memerlukan definisi yang berbeda, jika tidak, pola desain Pabrik tidak akan pernah digunakan. Tujuan saya adalah membuat pabrik sebodoh itu. Tapi saya lihat di sini bahwa tidak ada melarikan diri :-)
Gal Goldman