Diamond of Death?



  • struct virtual_base {
       virtual void foo() = 0;
       ~virtual_base() = default;
    };
    
    struct bar : virtual_base { };
    struct foobar : virtual_base { };
    
    struct derived : bar, foobar { };
    

    Bekomme ich hier wegen des vptrs oder der vtable in der Base-Klasse einen Diamond of Death? Muss ich dann Virtuelle Vererbung bei derived nutzen?



  • Diamant schrieb:

    Bekomme ich hier wegen des vptrs oder der vtable in der Base-Klasse einen Diamond of Death?

    Das sind Implementierungsdetails virtueller Funktionen und daher für eventuelle Probleme irrelevant.

    Grundsätzlich gibt's für virtuelle Funktionen die selben Probleme wie für "normale". Zusätzlich kannst du sie in derived nicht überschreiben, da nicht klar ist, welche dann gemeint ist.

    In deinem konkreten Beispiel treten keine Probleme auf, außer dass man derived nicht instanziieren kann.

    Dein Destruktor in virtual_base ist sinnlos. Er sollte doch wohl virtuell sein.



  • Dein jetziger Code verwendet die "Normale Mehrfachvererbung", s. Diamond-Problem, d.h. ein Objekt vom Typ "derived" hat dann intern zwei "virtual_base"-Objekte.
    Erst durch virtuelle Vererbung entsteht die "Diamond-Vererbung", d.h. "bar" und "foobar" teilen sich dann ein "virtual_base"-Objekt.



  • Hm, also brauche ich wohl virtuelle Vererbung dafür.
    Was ist besser: Code-Duplizierung (ca. 100 Zeilen), da ich Vererbung nutze, um selbige zu vermeiden und da es mit der "ist-ein"-Beziehung sematisch auch passt oder Virtuelle Vererbung in Kauf nehmen?

    Die Duplizierung würde ich dann so aufbrechen:

    struct A { };
    struct D1 : A { };
    struct D2 : A { };
    
    struct D21 : D2 { /* wiederhole hier Code aus D1 */ }
    
    // vs.
    
    struct D1 : virtual A { };
    struct D2 : virtual A { };
    struct D21 : D1, D2 { };
    

    A soll keine Membervariablen, aber rein-virtuelle Memberfunktionen haben.



  • In 95% der Fälle gibt's ein andere Entwurfsmuster, welches das Diamond of Death schöner löst.
    Ansonsten musst du halt entscheiden, was dir lieber ist, Performance oder mehr weniger Code.



  • In 95% der Fälle gibt's ein andere Entwurfsmuster, welches das Diamond of Death schöner löst.

    Zum Bleistift?
    Sowohl A als auch D1 werden niemals instanziiert, da beide rein virtuell sind. Instanzen von D2 und D21 hingegen gibt es. Wegen Laufzeitpolymorphie brauche ich A und D1, D1 brauche ich auch, um Template-Parameter zu verdecken:

    struct A { };
    
    struct D1 : virtual A { /* rein virtuell */ }
    
    template <typename T>
    struct D2 : virtual A { /* instanziierbar */ }
    
    template <typename T>
    struct D21 : D1, D2 { };
    


  • Auf 95% komme ich, da ich diese Art der Abhängigkeit 1. selber noch nie gebraucht habe (und wenn ich mal gemeint habe, ich brauche virtuelle Vererbung, habe ich doch noch ein anderes Entwurfsmuster gefunden) und 2., da du wenn du googelst "How to solve a Diamond of Death" du immer jemanden findest, der dir rät, ein anderes Entwursmuster zu suchen.
    Wenn du keines findest, musst halt dynamisch binden.



  • @Diamant
    Um ein anderes Design vorschlagen zu können, muss man den konkreten Anwendungsfall kennen 😉
    A, D1, D2 und D21 sind dazu viel zu abstrakt.



  • Keine Ahnung, was ihr wissen wollt. Wie würdet ihr denn das modellieren bzw. die Mehrfachvererbung aufheben:

    // Virtuelle Destruktoren mal weggelassen...
    struct Objekt { virtual void fun() = 0; };
    struct Waffe : Objekt { void angreifen(); }; // Zum Glück ist der Typ nicht instanziierbar
    struct Schneidewerkzeug : Objekt { 
       void fun() override;
       void klinge_wechseln();
    };
    struct Messer : Waffe, Werkzeug { }; // Mit einem Messer kann man nun mal angreifen, man kann es aber seine Klinge wechseln
    

    Objekt und Waffe brauche ich als polymorphe Zeiger, werden aber nie instanziiert.



  • Diamant schrieb:

    Keine Ahnung, was ihr wissen wollt.

    Na einen konkreten Fall. Idealerweise einen nicht erfundenen. Und idealerweise ohne Dummy-Funktionsnamen ala "fun" so dass man keine Ahnung für was sie stehen sollen.
    Keine Ahnung was da unklar sein könnte. 🙂

    Diamant schrieb:

    Wie würdet ihr denn das modellieren bzw. die Mehrfachvererbung aufheben:
    (...)
    Objekt und Waffe brauche ich als polymorphe Zeiger, werden aber nie instanziiert.

    Da gibt's mehrere Möglichkeiten.

    1. Mehrfachvererbung gar nicht aufheben, nix weiter machen.
      Je nachdem was Objekt enthält und was fun() macht kann es egal sein dass es mehrere Kopien von Objekt gibt. Wenn Objekt keine Membervariablen hat ist das oft der Fall.
      COM z.B. verwendet keine virtuelle Vererbung, dafür aber oft mehrfachvererbung. Die Klasse IUnknown wird dabei z.B. sehr oft vielfach vererbt.

    2. Waffe und Schneidewerkzeug müssen nicht unbedingt von Objekt erben. Die Regel dass alles was von Waffe und/oder Schneidewerkzeug abgeleitet ist auch von Objekt abgeleitet sein muss reicht völlig aus. Wenn man dann nen Waffe* hat aber nen Objekt* braucht muss man halt dynamic_cast verwenden.

    Um beurteilen zu können was mehr Sinn macht müsste man aber wissen was Objekt alles enthält/ist/kann.



  • Was hälst du von type erasure bzw. ich nenne sie hier einfach mal "external polymorphie".

    struct Objekt { virtual void fun() = 0; };
    struct Messer : Objekt
    {
        void fun() override;
        void klinge_wechseln();
        void angreifen();
    };
    
    class Schneidewerkzeug
    {
    public:
        template <class T>
        Schneidewerkzeug(T &obj)
        : fun_({](Objekt &obj){static_cast<T&>(obj).klinge_wechseln();}), obj_(obj) {}
    
        void klinge_wechseln()
        {
            fun_(*obj_);
        }
    
    private:
       using function = void(*)(Objekt &obj);
       function fun_;
       Objekt *obj_;
    };
    
    // Analog für Waffe
    

    Schneidewerkzeug kann nun Messer und jede andere Klasse derived from Objekt nehmen, die die Funktion klinge_wechseln() anbietet.
    Das einzige Problem hierbei ist, dass du nun bei einer Sammlung von Objekt Pointern, sie manuell zu ihrem echten Typen casten musst, wenn du welche erzeugst.
    Allerdings gibt es ein ähnliches auch mit deinem bisherigen Ansatz.



  • Für die Formatierung des Ctors (nebenbei: Tippfehler, fun_({](Objekt muss fun_([](Objekt heissen) würd ich dir, wärst du ein Mitarbeiter in meiner Abteilung, nen ordentlichen Rüffel verpassen.



  • Jaa, ich mir auch. Mache ich normalerweise auch schöner.



  • @Diamant: such mal nach "component based design/architecture" (bzw. "component based game engine"). Der Ansatz mittels Mehrfachvererbung ist hier viel zu unflexibel, daher ist man auf den komponentenbasierten Ansatz gekommen.

    Edit:
    Hier noch ein Link dazu: Component based game engine design


Anmelden zum Antworten