std::initializer_list mit Objekten und Vererbung



  • Hi,

    ich habe folgenden Beispielcode:

    #include <iostream>
    
    class animal
    {
        public:
            virtual void noise() const
            {
                std::cout << "default noise" << std::endl;
            }
    };
    
    class dog : public animal
    {
        public:
            void noise() const override
            {
                std::cout << "BARK" << std::endl;
            }
    
    };
    
    class cat : public animal
    {
        public:
            void noise() const override
            {
                std::cout << "MEOW" << std::endl;
            }
    };
    
    void f(std::initializer_list<animal> animals)
    {
        for(auto& a : animals)
            a.noise();
    }
    
    int main()
    {
        f({ dog(), cat() }); 
        //Output:
        //default noise
        //default noise
    }
    

    Was ich eigentlich haben möchte ist, dass die Funktion f() die Funktion noise() der jeweiligen Unterklasse automatisch aufruft. Letzterer Teil ist wichtig, da ich an einer Domain-Specific Language bastel und ich viele Unterklassen haben werde, weshalb manuelles Downcasting wegfällt.

    Bei Zeigern und Referenzen kann der Compiler das ja automatisch. Allerdings scheint es sowas wie initializer_lists mit Referenzen oder gar rvalue-Referenzen nicht zu geben, da die intern einfach nur ein Array vom Typ T sind. Die rvalue-Referenzen wären eigentlich ideal da das wie gesagt für eine DSL ist. Deshalb fallen auch Zeiger weg.

    Gibt es irgendeine Möglichkeit ohne manuelles Casten die Funktion f() dazu zu bringen, nicht animal.noise() aufzurufen?



  • *Edit: Ich habe die Beschreibung des Problems nicht so genau gelesen. Und meine Antwort zielt nicht so direkt auf deine Frage bezüglich std::initializer_list mit Referenzen/Pointer. Ich lasse sie dennoch stehen, vielleicht hilft sie jmd. anderem.
    *
    "Manuelles" casten würde sowieso nicht helfen, da hier slicing stattfindet. Wobei "sclicing" ein doofer Begriff ist - eigentlich wird bei der Übergabe der Objekte vom Typ cat und dog jeweils einfach wieder ein animal Objekt erstellt - du hast also in f() gar keine cat bzw. dog Objekte mehr, sondern nur noch animal Objekte.

    Das heisst, an f() musst du die von Objekte als Referenzen/Pointer übergeben.

    Bsp:

    #include <iostream> 
    #include <vector>
    
    class animal 
    { 
        public: 
            virtual void noise() const 
            { 
                std::cout << "default noise" << std::endl; 
            } 
    }; 
    
    class dog : public animal 
    { 
        public: 
            void noise() const override 
            { 
                std::cout << "BARK" << std::endl; 
            } 
    
    }; 
    
    class cat : public animal 
    { 
        public: 
            void noise() const override 
            { 
                std::cout << "MEOW" << std::endl; 
            } 
    }; 
    
    void f(const std::vector<animal*>& animals) 
    { 
        for(auto& a : animals) 
            a->noise(); 
    } 
    
    int main() 
    { 
        dog my_dog;
        cat my_cat;
        std::vector<animal*> animals { &my_dog, &my_cat };
        f(animals);
    }
    


  • bullete schrieb:

    Gibt es irgendeine Möglichkeit ohne manuelles Casten die Funktion f() dazu zu bringen, nicht animal.noise() aufzurufen?

    Wie theta schon sagte, hier hilft kein cast, in f hast du tatsächlich nur Objekte vom Typ animal.

    Eine Lösung mit std::initializer_list sehe ich nicht, aber für beliebig lange Listen würde mir eine Templatelösung einfallen:

    #include <iostream>
    
    class animal
    {
        public:
            virtual void noise() const
            {
                std::cout << "default noise" << std::endl;
            }
    };
    
    class dog : public animal
    {
        public:
            void noise() const override
            {
                std::cout << "BARK" << std::endl;
            }
    
    };
    
    class cat : public animal
    {
        public:
            void noise() const override
            {
                std::cout << "MEOW" << std::endl;
            }
    };
    
    template<typename A>
    void f( const A& a ) {
            a.noise();
    }
    
    template<typename A, typename... Animals>
    void f( const A& a, Animals... animals) {
            f(a); 
            f(animals...);
    }
    
    int main()
    {
        f(dog(), cat(), cat(), cat(), dog());
    }
    

  • Mod

    Warum kopierst du die Argumente die via das Pack genommen werden, aber das erste nicht? Wir brauchen hier keine Rekursion:

    (animals.noise(), ...);
    


  • Arcoth schrieb:

    Warum kopierst du die Argumente die via das Pack genommen werden, aber das erste nicht? Wir brauchen hier keine Rekursion:

    (animals.noise(), ...);
    

    Weil das nur auf richtigen Computern geht. Selbst VS2017 kann es AFAIK nicht.


  • Mod

    manni66 schrieb:

    Arcoth schrieb:

    Warum kopierst du die Argumente die via das Pack genommen werden, aber das erste nicht? Wir brauchen hier keine Rekursion:

    (animals.noise(), ...);
    

    Weil das nur auf richtigen Computern geht. Selbst VS2017 kann es AFAIK nicht.

    Ist ja auch kein Standard, vorerst.
    Man kann sich mit Initialisierung behelfen:

    char foo[] = { (animals.noise(),0)... };
    

    und ggf. noch ein

    (void)foo;
    

    damit der Compiler nicht wegen ungenutzter lokaler Variablen warnt. Diesbzgl. etwas schöner ist

    struct sequential_eval {
        template <typename... T>
        constexpr sequential_eval(T&&...) noexcept {}
    };
    

    mit

    sequential_eval{ (animals.noise(),0)... };
    

    setzt aber hinreichend aktuelle Compiler voraus. Obwohl schon in C++11 spezifiziert, setzt z.B. gcc das erst ab Version 5 korrekt um (Auswertungsreihenfolge). Angenehmer Nebeneffekt einer solchen Klasse: wenn man irgendwann mal auf Fold-Expressions wechseln will, kann man alle relevanten Stellen durch einfach Suche finden.


  • Mod

    camper schrieb:

    Ist ja auch kein Standard, vorerst.

    Keine Sorge, dauert nur noch ein paar Monate. Und meine Antworten richten sich kategorisch nicht an solche, die keine aktuelle Clang oder GCC Installation verwenden.



  • Arcoth schrieb:

    camper schrieb:

    Ist ja auch kein Standard, vorerst.

    Keine Sorge, dauert nur noch ein paar Monate. Und meine Antworten richten sich kategorisch nicht an solche, die keine aktuelle Clang oder GCC Installation verwenden.

    Ein Hinweis welche Version des standards benötigt wird sollte man schon angeben


  • Mod

    Ja, aber dann müsste ich das auch bei C++14 features machen, die VC++ sicher auch nicht alle implementiert hat, etc.

    Gut, vielleicht sollte man das Feature beim Namen nennen, damit man im Zweifelsfall googlen kann, ob es von der eigenen Implementierung unterstützt wird. Im obigen Code wird eine fold expression angewandt.



  • Vielen Dank für die Antworten. Da es sich nur um ein Experiment gehandelt hat, hab ich das ganze ein bisschen hässlich gelöst.

    Um beim Beispiel zu bleiben - Animal hat eine std::function()-Memberfunktion bekommen, und jede Unterklasse hat diese Memberfunktion auf das gesetzt was es wollte. Das waren dann Lambdas in Lambdas und relativ hässlich eben, aber hat den Zweck erfüllt.

    Auch beim öh hochcasten/reduzieren auf Animal wieder blieb damit die veränderte Funktionalität erhalten und ich konnte so die jeweilige Spezialfunktionalität der Unterklasse ausführen.

    Ich hab dann allerdings relativ schnell gemerkt, dass die DSL zwar funktioniert wie gewollt, aber in meinem Anwendungsfall das ganze absolut nicht besser macht, und somit hat sich das Experiment dann auch erledigt. War aber mal ein interessanter Ausflug.



  • Falls jemand das Endresultat interessiert vom Aussehen:

    gui_window
        {   
            "This is a test",
            {
                button { "Click me 1", []() { std::cout << "Clicked 1" << std::endl; }}, 
                button { "Click me 2", []() { std::cout << "Clicked 2" << std::endl; }}, 
                listbox<std::string>
                {
                    "Some items",
                    { "This", "is", "a","test" },
                    selected_item
                },
                listbox<int>
                {
                    "Some integers",
                    {1,2,3,4},
                    selected_item
                },
                tree
                {
                    "Test stuff here",
                    {
                        button { "Click me please", []() { std::cout << "Tree button!" << std::endl; }}, 
                        tree
                        {
                            "Second tree",
                            {
                                bullet{}
                            }
                        }
                    }
                }
            }
        };
    

    Das ist eine embedded DSL, um ein GUI-Toolkit zu bedienen. Das da oben beschreibt z.B. ein Fenster mit zwei Buttons, einer Listbox, dann einem Tree in dem wiederum ein Button und ein Untertree sind usw.

    Das alles sind nur verschachtelte ctor-Aufrufe von Objekten. Im Hintergrund werden die GUI-Funktionen von "dear ImGui" aufgerufen. Aber wie gesagt, es funktioniert wie ich mir das vorgestellt hatte, aber schön ist anders 😉


Anmelden zum Antworten