Kann ich mit der typeid einer Klasse eine Instanz erzeugen



  • Hallo..

    Folgendes Problem. Ich würde gerne über eine map festlegen welche Instanz welcher Klasse erzeugt wird
    So habe ich mir das in etwa vorgestellt:

    enum class MyEnumType
    {
         FirstValue,
         SecondValue,
         ThirdValue
    }
    
    public class BaseClass
    {
         BaseClass(Parameter parameter);
    }
    public class TheFirstClass : public BaseClass
    {
         // ctor
    }
    
    public class TheSecondClass : public BaseClass
    {
      // ctor
    }
    
    public class TheThirdClass : public BaseClass
    {
      // ctor
    }
    
    
    constexpr std::map<MyEnumType, std::type_index> factorymap
    {
          { MyEnumType::FirstValue, typeid(TheFirstClass) } ,
          { MyEnumType::SecondValue, typeid(TheSecondClass) },
          { MyEnumType::thirdValue, typeid(TheThirdClass) }
    };
    
    public class Factory
    {
        static shared_ptr<BaseClass>Factory::Create(MyEnumType type, Parameter parameter)
        {
           auto instance = std::make_shared<decltype(factorymap.at(type))>(parameter);
    
           return instance;
        }
    }
    


  • Nein kannst du nicht. C++ hat kein Reflection System, mit dem das möglich wäre.
    Denn zur laufzeit existieren deine Klassen nicht mehr so wie im code.



  • @firefly ok

    gibt es da irgend eine andere Möglichkeit. Damit ich kein switch case auf den enum type machen muss?

    Ist das bei emplace() bei einer List nicht auch so dass ich hier irgendwie der Constructor aufgerufen wird.



  • @booster

    Warum möchtest du keinen switch-case Block? Du könntest dir was mit type traits basteln, falls das nicht zu viele unterschiedliche Typen werden, aber das ist letzendlich auch wieder nur eine Fallunterscheidung.

    #include <memory>
    
    // nicht-implementiertes Klassen-Template sorgt für Compiler-Fehler
    template<unsigned int> struct TypeIdentifier;
    
    template<> struct TypeIdentifier<MyEnumType::FirstValue>
    {
       using type = TheFirstClass;
    };
    
    template<> struct TypeIdentifier<MyEnumType::SecondValue>
    {
       using type = TheSecondClass;
    };
    
    template<> struct TypeIdentifier<MyEnumType::ThirdValue>
    {
       using type = TheThirdClass;
    };
    
    
    template<unsigned int N>
    using type_identifier_t = typename TypeIdentifier<N>::type;
    
    int main()
    {
       // erzeugt ein TheFirstClass Objekt
       auto p = std::make_shared<type_identifier_t<MyEnumType::FirstValue>>();
    }


  • @booster sagte in Kann ich mit der typeid einer Klasse eine Instanz erzeugen:

    Ist das bei emplace() bei einer List nicht auch so dass ich hier irgendwie der Constructor aufgerufen wird.

    Das ist was anderes, in einer std::list hält man keine polymorphen Objekte.



  • @booster Es wäre gut zu wissen, ob der Typ statisch zur Compile-Zeit angegeben werden soll oder dynamisch zur Laufzeit. Im ersteren Fall sehe ich in dem enum keinen Vorteil dazu, den Typen direkt anzugeben. Letztendlich macht man damit MyEnumType::FirstValue de facto nur zu einem Alias für TheFirstClass. Es sei denn natürlich ich habe irgendeinen nützlichen Use Case übersehen. @DocShoe hat dir ein Beispiel aufgezeigt, wie man das umsetzen könnte.

    Wenn du die Typen dynamisch zur Laufzeit angeben möchtest, könnte man das z.B. mit polymorphen Funktionsobjekten in der map machen, welche jeweils eine Instanz des gewünscheten Typs erzeugen:

    #include <iostream>
    #include <memory>
    #include <map>
    
    enum class MyEnumType
    {
        FirstValue,
        SecondValue,
        ThirdValue
    };
    
    using Parameter = int;
    
    struct BaseClass
    {
        virtual ~BaseClass() = default;
        virtual void hello() = 0;
    }; 
    
    struct TheFirstClass : BaseClass
    {
        TheFirstClass(Parameter p)
        {
            std::cout << "TheFirstClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheFirstClass!" << std::endl;
        }
    };
    
    struct TheSecondClass : BaseClass
    {
        TheSecondClass(Parameter p)
        {
            std::cout << "TheSecondClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheSecondClass!" << std::endl;
        }    
    };
    
    struct TheThirdClass : BaseClass
    {
        TheThirdClass(Parameter p)
        {
            std::cout << "TheThirdClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheThirdClass!" << std::endl;
        }        
    };
    
    struct CreateBase
    {
        virtual ~CreateBase() = default;
        virtual auto operator()(const Parameter&) -> std::shared_ptr<BaseClass> = 0;
    };
    
    template <typename T>
    struct Create : CreateBase
    {
        auto operator()(const Parameter& p) -> std::shared_ptr<BaseClass> override
        {
            return std::make_shared<T>(p);
        }
    };
    
    std::map<MyEnumType, std::unique_ptr<CreateBase>> factorymap;
    
    template <typename T>
    void register_class(MyEnumType key)
    {
        factorymap.emplace(
            std::pair<MyEnumType, std::unique_ptr<CreateBase>>{ 
                key,
                std::make_unique<Create<T>>()
            }
        );
    }
    
    auto create(MyEnumType key, Parameter p) -> std::shared_ptr<BaseClass>
    {
        return (*factorymap[key])(p);
    }
    
    auto main() -> int
    {
        register_class<TheFirstClass>(MyEnumType::FirstValue);
        register_class<TheSecondClass>(MyEnumType::SecondValue);
        register_class<TheThirdClass>(MyEnumType::ThirdValue);
        
        create(MyEnumType::FirstValue, 1)->hello();
        create(MyEnumType::SecondValue, 2)->hello();
        create(MyEnumType::ThirdValue, 3)->hello();
    }
    

    Ausgabe:

    TheFirstClass(1)
    Hello from TheFirstClass!
    TheSecondClass(2)
    Hello from TheSecondClass!
    TheThirdClass(3)
    Hello from TheThirdClass!
    

    Lauffähiges Beispiel in Godbolt: https://godbolt.org/z/714j87fr6

    Die Map hält dabei (unique) Pointer auf CreateBase-Objekte, die jeweils den dynamischen Typen Create<T> haben, wobei T der gewünschte Typ ist, der erzeugt werden soll.

    Das ganze hat natürlich den Nachteil, dass die erzeugten Objekte keine beliebigen Konstruktoren haben können. Hier in dem Fall müssen alle einen Konstruktor der Form T(Parameter p) haben. Auch der Zugriff erfolgt über die bekannte Member-Funktion hello der Basisklasse. Natürlich kann man einen Downcast machen, dafür muss man aber genau wissen, was für ein Typ hinter dem BaseClass-Pointer tatsächlich steckt.



  • Ich bin ja im Allgemeinen ein grosser Fan von expliziten Interface-Klassen statt std::function - aber das ist ein Fall wo ich wirklich mal std::function nehmen würde.



  • @hustbaer sagte in Kann ich mit der typeid einer Klasse eine Instanz erzeugen:

    Ich bin ja im Allgemeinen ein grosser Fan von expliziten Interface-Klassen statt std::function - aber das ist ein Fall wo ich wirklich mal std::function nehmen würde.

    Hah. Stimmt. Ich war so fixiert auf die Polymorphie um die Typinformation loszuwerden, dass ich überhaupt nicht auf dem Schirm hatte, dass std::function sowas direkt "eingebaut" hat. Danke, das ist deutlich kompakter:

    #include <iostream>
    #include <memory>
    #include <map>
    #include <functional>
    
    enum class MyEnumType
    {
        FirstValue,
        SecondValue,
        ThirdValue
    };
    
    using Parameter = int;
    
    struct BaseClass
    {
        virtual ~BaseClass() = default;
        virtual void hello() = 0;
    }; 
    
    struct TheFirstClass : BaseClass
    {
        TheFirstClass(Parameter p)
        {
            std::cout << "TheFirstClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheFirstClass!" << std::endl;
        }
    };
    
    struct TheSecondClass : BaseClass
    {
        TheSecondClass(Parameter p)
        {
            std::cout << "TheSecondClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheSecondClass!" << std::endl;
        }    
    };
    
    struct TheThirdClass : BaseClass
    {
        TheThirdClass(Parameter p)
        {
            std::cout << "TheThirdClass(" << p << ")" << std::endl;
        }
        
        void hello() override
        {
            std::cout << "Hello from TheThirdClass!" << std::endl;
        }        
    };
    
    std::map<MyEnumType, std::function<std::shared_ptr<BaseClass>(Parameter)>> factorymap;
    
    template <typename T>
    void register_class(MyEnumType key)
    {
        factorymap.emplace(
            key,
            [](Parameter p){ return std::make_shared<T>(p); }
        );
    }
    
    auto create(MyEnumType key, Parameter p)
    {
        return factorymap[key](p);
    }
    
    auto main() -> int
    {
        register_class<TheFirstClass>(MyEnumType::FirstValue);
        register_class<TheSecondClass>(MyEnumType::SecondValue);
        register_class<TheThirdClass>(MyEnumType::ThirdValue);
        
        create(MyEnumType::FirstValue, 1)->hello();
        create(MyEnumType::SecondValue, 2)->hello();
        create(MyEnumType::ThirdValue, 3)->hello();
    }
    

    https://godbolt.org/z/of93coz5v



  • Achja... beim Map-Lookup sollte man vielleicht eher at verwenden. Sonst wird beim Aufruf mit einem nicht enthaltenen Key ein unnötiger Eintrag erzeugt. Was dann automatisch auch zur Folge hat dass die Sache nicht mehr threadsafe ist.



  • Hallo zusammen.

    Danke für eure Antworten. Ok in der Tat hatte ich das mit einer Register und Create auch schon im Kopf.
    Ein bisschen anders und nicht ganz zu Ende gedacht. Aber super ja so kann es gehen.

    Der Enum wird mir von einem externen System geliefert. Darauf muss ich reagieren und die entsprechenden Klassen erzeugen. Darum das Mapping.



  • Wenn das eine konkrete Anwendung ist die du da schreibst, wo du den "Klasse anhand von enum Wert auswählen" Code genau 1x brauchst, dann würde ich switch verwenden. Weil es viel weniger Code braucht - und normalerweise auch schneller sein sollte.



  • Es geht halt darum dass der Enum und die Klassen später erweitert werden müssen. Da ist das mit dem switch case immer etwas unschön. Aber ja kann schon schneller sein.

    Aber ist der Fall wirklich so ungewöhnlich? Das ist doch das Factory Pattern.



  • @booster

    Die Frage ist, warum du keinen switch-case Block benutzen möchtest? Was spricht dagegen?



  • @booster sagte in Kann ich mit der typeid einer Klasse eine Instanz erzeugen:

    Es geht halt darum dass der Enum und die Klassen später erweitert werden müssen. Da ist das mit dem switch case immer etwas unschön.

    Es muss immer irgendwo irgendwas erweitert werden. So lange der Code sich im selben Modul befindet wie der enum, ist es mMn. mehr oder weniger egal ob man nen switch oder ne map verwendet. Bzw. switch hat sogar nen Vorteil: mit den passenden Compiler-Switches bekommst du eine Warnung wenn du den enum erweiterst, aber vergisst den case beim switch dazuzumachen.

    Bei GCC ist das z.B. -Wswitch (enthalten in -Wall).
    Bei MSVC musst du die extra aufdrehen, z.B. mit /W4 /w44061 /w44062.

    Beispiel:

    enum class Foo {
        A,
        B,
        C,
    };
    
    int ok(Foo f) {
        switch (f) {
        case Foo::A: return 1;
        case Foo::B: return 2;
        case Foo::C: return 3;
        }
    
        return 0;
    }
    
    int warn(Foo f) {
        switch (f) {
        case Foo::A: return 1;
        case Foo::B: return 2;
        }
    
        return 0;
    }
    

    https://godbolt.org/z/cd49vxrhz

    Wichtig dabei ist dass es kein default Label gibt -- denn sonst kommt bei GCC keine Warnung.

    Aber ja kann schon schneller sein.

    Aber ist der Fall wirklich so ungewöhnlich? Das ist doch das Factory Pattern.

    Ungewöhnlich nicht. Die Frage ist für mich aber ob du diese Komplexität brauchst. Also ob sie einen Vorteil bringt.

    Es gibt natürlich Fälle wo das Sinn macht bzw. sogar notwendig ist. Stell dir z.B. ein GUI Framework vor, welches zur Laufzeit anhand von "Beschreibungen" (XML Files, ...) GUI Elemente zusammenbaut. Sowas sollte natürlich von ausserhalb des Frameworks erweiterbar sein, damit man seine eigenen Control-Klassen implementieren und verwenden kann. Da funktioniert natürlich ein einfaches switch-case im Framework-Code nicht. Denn das Framework kann ja die Control-Klassen die man selbst dazubastelt nicht kennen.

    Bzw. auch bei Anwendungen kann es Sinn machen. Ab einer bestimmten Grösse ist es u.U. nicht mehr wünschenswert wenn alle abgeleiteten Klassen an einem zentralen Punkt bekannt sein müssen.

    Aber so lange alles halbwegs übersichtlich bleibt, gibt es mMn. keinen echten Vorteil durch die Verwendung einer map. Echte Nachteile aber schon (z.B. komplexer, keine Warnings wenn man was vergisst).


Anmelden zum Antworten