Ist OOP in C++ eigentlich noch guter Stil?



  • Hallo Forum,

    Ich wollte mal Wissen, ob Klassenhierarchien, Vererbung, etc. noch guter Stil ist oder ob man so was besser mittels freier Templatefunktionen löst?

    Wenn beispielsweise ein Strategiespiel programmieren will ist es besser, wenn ich alle Figuren OOP-mäßig von eine Basisklasse ableite (Variante1), oder doch lieber alles mittels Template löse (Variante2)? 😕

    //Variante 1
    struct Spielfigur {
      Spielfigur(); // usw.
      virtual void move();
      virtual void fire();  // ...
      int health; // ...
    };
    
    struct Soldat : Spielfigur { };
    struct Panzer : Spielfigur {
      void fire() ;     // Spezialisierung
    };
    // ...
    
    //Variante 2
    struct Soldat { 
      //...
      int health;
    };
    
    struct Panzer {
      // ...
    };
    
    template<class SF> void move(SF &);
    template<> void move<Panzer>(Panzer &);    // Spezialisierung;
    template<class SF> void fire(SF &);
    

    Gruß

    Kolonist



  • Da deine Hierachie tatsächlich eine "ist-ein" Beziehung ist würde ich klassisch vererben und keine templates benutzen (vgl. Liskovsches Substitutionsprinzip).



  • Das ist auch gar kein Anwendungsbereich von Templates. Wenn Panzer nicht von Spielfigur erbt, kannst Du gar keinen vector o.a. Container mit Spielfiguren anlegen. Das willst Du aber bestimmt häufig.

    Außerdem sind freie Funktionen mit Templateparametern nicht dasselbe, die haben ja gar keinen Zugriff auf die Interna. Außer Du machst alles mit friends voll, nene, das würde ich Mal schön bleiben lassen.

    Das ist ja auch der Witz: Über Template wird statische Polymorphie umgesetzt, mit Vererbung und virtuellen Funktionen wird dynamische Polymorphie umgesetzt. Ersteres ist sinnvoll, wenn Du dynamisch Code generieren willst, der aber von zur Kompilierzeit konstanten Eigenschaften abhängt. Bei letzterem kann es sich auch zur Laufzeit verändern.

    Ich würde Templates also nutzen, wenn alle notwendigen Daten bereits zur Kompilierzeit feststehen und das auch künftig wohl so bleiben wird. Da könntest Du nämlich auch dynamische Polymorphie einsetzen, doch eben da würde ich es nicht tun.

    Andersrum klappt das nicht. Was zur Laufzeit erst bekannt ist und zu einem anderen Aufruf führen kann, kannst Du nicht über Templates alleine lösen. Bedenke immer, dass Templates neuen Code für den Compiler und den Linker produzieren.



  • Danke für die Antworten

    Eisflamme schrieb:

    Das ist auch gar kein Anwendungsbereich von Templates. Wenn Panzer nicht von Spielfigur erbt, kannst Du gar keinen vector o.a. Container mit Spielfiguren anlegen. Das willst Du aber bestimmt häufig.

    Sicher. Da aber die Unterschiedlichen Figuren schon zur Compilezeit bekannt sind kann ich einfach mehrere Container anlegen.

    Außerdem sind freie Funktionen mit Templateparametern nicht dasselbe, die haben ja gar keinen Zugriff auf die Interna. Außer Du machst alles mit friends voll, nene, das würde ich Mal schön bleiben lassen.

    Wenn ich meine "structs" nicht als Klassen mit Zugriffsmodifizierer programmierere sondern eher als Datenstrukturen am Vorbild von C braucht's keine friends.



  • Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Ja, Datenstrukturen in C haben aber zahlreiche Nachteile. Beispielsweise wird Kapselung nicht umgesetzt, das hat massive Nachteile bzgl. Erweiterbarkeit und Wartbarkeit Deines Codes. Und da das hier keine reinen Datenhaltungsstrukturen sind, sondern auch Methoden angewandt werden sollen, bietet sich eine struct hier nicht an.

    OOP macht auch in C++ für größere Projekte viel Sinn. Das ist eines der wichtigsten (das wichtigste?) Paradigma für sauberen und wartbaren Code. Templates können hier unterstützen, v.a. für generische Bibliotheksfunktionen oder eben Optimierungen zur Compilezeit.

    Du erwirtschaftest Dir hier durch den Einsatz keine Vorteile und schaffst Dir auf der anderen Seite Nachteile, wie oben aufgeführt. Für den von Dir beschriebenen Anwendungsfall würde ich also Templates nicht verwenden.



  • Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Aber virtuelle Funktionen machen den Programmcode lahm.
    Wenn einem Geschwindigkeit egal ist, kann man auch Java nutzen.



  • Gomo schrieb:

    Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Aber virtuelle Funktionen machen den Programmcode lahm.
    Wenn einem Geschwindigkeit egal ist, kann man auch Java nutzen.

    Aber manchmal leider unvermeidlich, wenn man dynamische Polymorphie braucht.



  • Nathan_logoff schrieb:

    Gomo schrieb:

    Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Aber virtuelle Funktionen machen den Programmcode lahm.
    Wenn einem Geschwindigkeit egal ist, kann man auch Java nutzen.

    Aber manchmal leider unvermeidlich, wenn man dynamische Polymorphie braucht.

    Was ist statische Polymorphie?



  • Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Kann man auch auf eine Stelle reduzieren.
    Indem man ein "mach das für alle Spielfiguren" Funktions-Template baut (Template-Parameter: der auszuführende Funktor).

    Ansonsten geht glaube ich "component based game engine" ein wenig in die Richtung.
    Wobei man dort nicht verschiedene Komponenten für die einzelnen Spielfiguren hätte, sondern für verschiedene "Teile" (bzw. Aspekte) die in den Spielelementen vorkommen.



  • out schrieb:

    Nathan_logoff schrieb:

    Gomo schrieb:

    Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    Aber virtuelle Funktionen machen den Programmcode lahm.
    Wenn einem Geschwindigkeit egal ist, kann man auch Java nutzen.

    Aber manchmal leider unvermeidlich, wenn man dynamische Polymorphie braucht.

    Was ist statische Polymorphie?

    In etwa der zweite Ansatz vom TE.



  • out schrieb:

    Was ist statische Polymorphie?

    Bloss nicht googeln, sonst findest du es am ende noch raus.
    http://en.wikipedia.org/wiki/Template_metaprogramming#Static_polymorphism


  • Mod

    out schrieb:

    Was ist statische Polymorphie?

    Na, das was der TE hier wohl vor hat. In C++ in der Regel mittels CRTP umgesetzt:
    template <class Derived>

    Wikipedia schrieb:

    template <typename Derived> struct base
    {
        void interface()
        {
             // ...
             static_cast<Derived*>(this)->implementation();
             // ...
        }
    };
     
    struct derived : base<derived>
    {
         void implementation();
    };
    


  • CRTP hätte allerdings den Nachteil, dass ich für alle Figuren eine Funktion implementieren muss und nicht auf die Standardfunktion zurückgreifen kann.

    Soweit ich weiß ich mit CRTP auch keine Polymorphie bei tieferer Vererbung mehr möglich.

    Beispiel:

    struct Spielfigur {
     virtual void bewege();
    };
    
    struct Buerger : Spielfigur {
     void bewege() override;
    };
    
    struct Reservist : Buerger {
     void bewege() override;
    };
    

    Wüsste ich nicht, wie ich das mit CRTP umsetzen sollte.


  • Mod

    Kolonist schrieb:

    CRTP hätte allerdings den Nachteil, dass ich für alle Figuren eine Funktion implementieren muss und nicht auf die Standardfunktion zurückgreifen kann.

    Soweit ich weiß ich mit CRTP auch keine Polymorphie bei tieferer Vererbung mehr möglich.

    Beispiel:

    struct Spielfigur {
     virtual void bewege();
    };
    
    struct Buerger : Spielfigur {
     void bewege() override;
    };
    
    struct Reservist : Buerger {
     void bewege() override;
    };
    

    Wüsste ich nicht, wie ich das mit CRTP umsetzen sollte.

    😕 Das ist doch quasi eine Auflistung der Fälle, für die der Trick überhaupt erfunden wurde.

    #include <iostream>
    
    using namespace std;
    
    template <typename Derived> struct base
    {
      void interface()
      {
        static_cast<Derived*>(this)->implementation();
      }
      void implementation()
      {
        cout << "Default\n";
      }
    };
    
    struct overrider : base<overrider>
    {
      void implementation()
      {
        cout << "Overridden\n";
      }
    };
    
    struct nonoverrider : base<nonoverrider>
    {
    };
    
    template <typename twicederived> struct derived : base<twicederived> 
    {
      void implementation()
      {
        cout << "Derived Base\n";
      }
    };
    
    struct twicederivednonoverrider : derived<twicederivednonoverrider>
    {
    };
    
    struct twicederivedoverrider : derived<twicederivedoverrider>
    {
      void implementation()
      {
        cout << "Derived overridden\n";
      }
    };
    
    int main()
    {
      overrider overrider;
      nonoverrider nonoverrider;
      twicederivednonoverrider twicederivednonoverrider;
      twicederivedoverrider twicederivedoverrider;
    
      overrider.interface();
      nonoverrider.interface();
      twicederivednonoverrider.interface();
      twicederivedoverrider.interface();
    }
    

    https://ideone.com/YDFXmp

    P.S.: Das bedeutet aber nicht, dass ich das in diesem Fall empfehlen würde. Ich habe nur festgestellt, dass es wohl das ist, was du im Eingangsbeitrag suchst. Die wirkliche Empfehlung ist aber

    Eisflamme schrieb:

    Verschiedene Container für verschiedene Figurentypen? Super skalierbar. Sobald Du einen neuen Figurentyp hinzufügst, musst Du den neuen Container anlegen und bei allen Schleifen, die über die Container laufen, noch eine weitere für den neuen Container anlegen.

    virtual function overhead < mehrere container overhead

    P.P.S: Obige Ungleichung hängt natürlich vom Anwendungsfall ab, aber erfahrungsgemäß braucht das Verhältnis Klassen:Instanzen nicht groß zu sein, damit die Aussage stimmt.


  • Mod

    Kolonists Problem besteht vermutlich darin, dass er mit
    Spielfigur -> Buerger -> Reservist
    beabsichtigt, sowohl Buerger als auch Reservist als nichtabstrakte konkrete Klassen einzusetzen. Und das ist dann mit CRTP etwas schwieriger. Andererseits würde so eine Benutzung sowieso das LSP verletzen...



  • Ich denke, man kommt um virtuelle Funktionen gar nicht herum, wenn man nicht irgendwelche anderen Tricks einsetzen will, die aber auch keine/kaum Geschwindigkeitsvorteile bringen.
    Man weiß ja beim Compilen noch gar nicht, welchen Einheitentyp man vor sich hat.
    Es kann ja sein, dass der Spieler einen normalen Bürger, einen Soldaten oder einen Panzer baut. Da das dynamisch ist, müsste man das mit switch-case abfragen und da hat doch wohl eine virtuelle Funktion klare Vorteile, sowohl von der Übersicht als auch möglicherweise von der Geschwindigkeit.


  • Mod

    Marthog schrieb:

    Ich denke, man kommt um virtuelle Funktionen gar nicht herum, wenn man nicht irgendwelche anderen Tricks einsetzen will, die aber auch keine/kaum Geschwindigkeitsvorteile bringen.
    Man weiß ja beim Compilen noch gar nicht, welchen Einheitentyp man vor sich hat.
    Es kann ja sein, dass der Spieler einen normalen Bürger, einen Soldaten oder einen Panzer baut. Da das dynamisch ist, müsste man das mit switch-case abfragen und da hat doch wohl eine virtuelle Funktion klare Vorteile, sowohl von der Übersicht als auch möglicherweise von der Geschwindigkeit.

    Er hat etwas vor in der Art von

    vector<Bürger> bürger;
    vector<Soldat> soldaten;
    // usw. für alle Einheitentypen
    
    // Etwas, das mit allen Einheiten gemacht werden soll:
    for (auto einheit: bürger) einheit.tu_was();
    for (auto einheit: soldat) einheit.tu_was();
    // usw. für alle Einheitentypen
    

    Wobei die genaue Auswahl der tu_was-Funktion von der Vererbungshierarchie abhängen soll, insbesondere mit der Möglichkeit, Implementierungen zu erben oder zu überschreiben.



  • for (auto einheit: bürger) einheit.tu_was();
    

    Ich hoffe, dass diese Zeile pseudocode-mäßig gemeint ist ...



  • sind umlaute in c++ nicht ein absolutes no-go?



  • LOL! schrieb:

    sind umlaute in c++ nicht ein absolutes no-go?

    Das ist nicht das Problem.
    Sondern die Schleife an sich, das wäre im Bereich typischer Anfänger Fehler.


Log in to reply