Wann static Methoden verwenden?



  • Hallo!

    Mir ist klar, wie man static Methoden verwendet bzw. implementiert, mir ist hingegen nicht ganz klar, wann es design-mäßig günstig ist, diese zu verwenden.

    Ein Beispiel:
    Nehmen wir das allseits bekannte TicTacToe Spiel (http://de.wikipedia.org/wiki/Tic_Tac_Toe).
    Ich habe eine Klasse, die die Logik des Spiels mitsamt KI implementiert.
    Um festzustellen, ob in einer gewissen Spielsituation ein Spieler gewonnen hat, bzw. ob es unentschieden (sprich: alle Felder voll, aber kein Gewinner) steht, habe ich die Funktion FindWinner. Da ich hierbei auf keine Membervariablen zugreife, sondern ein Spielfeld übergebe, habe ich die Funktion static gemacht.
    Gleiche Überlegung liegt auch den Funktionen SetPlayGround und CalcRating zugrunde, wobei erstere ein Spielfeld einem anderen zuweist, während letztere eine Spielsituation bewertet.
    Jedes mal handelt es sich um eine Funktionalität, welche nicht von Membervariablen abhängt - also allgemein für die Klasse ist.

    Ist das tatsächlich ein Anwendungsfall für static Methoden, oder würdet ihr static Methoden eher in anderen Fällen verwenden - und wenn ja - in welchen?

    class CTicTacToe : public ITicTacToe
    {
    
    public:
        explicit        CTicTacToe(IViewer* aViewer);
    
        virtual void    ResetGame();
        virtual bool    MarkSpace(int x, int y);
        virtual bool    AIPlay(EDifficulty difficulty);
        virtual EPlayer GameFinished();
        virtual EPlayer GetPlayerAt(int x, int y);
    
    protected:
        static EPlayer  FindWinner(const EPlayer aPlayGround[][PlayGroundSize]); // <-- hier
        void            AssertIndex(int index);
        void            InitPlayGround();
        CKIResult       AIFindBestPlay(int depth, const EPlayer aPlayGround[][PlayGroundSize]);
        static void     SetPlayGround(EPlayer dest[][PlayGroundSize], const EPlayer src[][PlayGroundSize]); // <-- hier
        static double   CalcRating(EPlayer aPlayer, int depth); // <-- hier
    
    private:
        IViewer*        m_Viewer;
        EPlayer         m_PlayGround[PlayGroundSize][PlayGroundSize];
        int             m_AIMaxDepth;
    
    };
    


  • Du bist irgendwie noch nicht ganz in C++ angekommen. Man hat es da nicht so mit Interfaces und Pointern und (Achtung Tautologie) monolithischen, mega-mässigen Gottes-Klassen ("Klasse, die die Logik des Spiels mitsamt KI implementiert").

    Um die Frage zu beantworten: Ja die Funktionen sollten keine nicht-statische Memberfunktion sein, allerdings auch keine statische, sondern im Allgemeinen lieber eine freie Funktion.

    Dein Design würde in C++ so aussehen:

    class TTTField {
        EPlayer m_PlayGround[PlayGroundSize][PlayGroundSize];
    
    public:
        bool    MarkSpace(int x, int y);
        EPlayer GetPlayerAt(int x, int y);
    };
    
    // ResetGame fällt weg, da nimmt man einfach ein neues Spielfeld
    // AssertIndex verstehe ich nicht
    
    bool AIPlay(TTTField const& f, EDifficulty difficulty);
    EPlayer  FindWinner(TTTField const& f); // <-- hier
    CKIResult AIFindBestPlay(TTTField const& f, int depth);
    void SetPlayGround(TTTField const& a, TTTField const& b); // <-- hier
    double CalcRating(EPlayer aPlayer, int depth); // <-- hier
    

    Kurze Erklärung zu Pointern: Pointer zu verwalten führt sehr leicht zu Leaks. Daher NIE besitzende Zeiger verwenden. Wenn du einen halbwegs aktuellen COmpiler zur Hand hast, kannst du std::unique_ptr<> verwenden. Arrays werden praktisch immer in einem std::vector gehalten.

    Kurze Erklärung zu Interfaces: Ist in C++ meistens nicht nötig, da man mit Templates ein Sprachmittel zur Verfügung hat, das die meisten Fälle abdecken kann und Programmierung gegen Interfaces zulässt, aber während der Laufzeit keinen Overhead verursacht.

    Daher hat man in C++ keine tiefgreifende Klassenhierarchien und die Klassen sind eher klein. Man sagt: Freie Funktionen erhöhen die Kapselung und bedeuten daher sogar mehr OOP. Mach möglichst viele möglichst kleine Klassen, die lose gekoppelt sind und bearbeite sie mit freien Funktionen. Vererbung ist eher rar.



  • Danke für die AW. Interessanter Lösungsvorschlag!

    Zu den Interfaces:
    Hab das Buch "Design Patterns: Elements of Reusable Object-Oriented Software" gelesen, und dort wurde auch bei C++ Code immer mit (expliziten) Interfaces gearbeitet.
    Ist in C++ aber trotzdem von (expliziten) Interfaces abzuraten?

    Zu std::vector und 2d Arrays:
    Das ganze würde ja zu vector< vector<int> > führen.
    Ist das performant? Wäre es evtl. besser, an der Stelle boost::multi_array zu verwenden?

    Zu den Zeigern:
    Die Instanz von CTicTacToe ist nicht Besitzer vom übergebenen IViewer Zeiger, daher wäre es an der Stelle fatal, diesen Zeiger am Ende der Lebensdauer zu löschen.
    Als Referenz kann ich den IViewer aber auch nicht übergeben/speichern, da auch ein 0 Zeiger möglich sein soll. Daher geht ein roher Zeiger an der Stelle schon ok.



  • Noch was zu den "monolithischen, mega-mässigen Gottes-Klassen":
    Kennst du ein gutes Buch bzw. eine Website, die das von dir vorgeschlagene Design (also kleine Klassen mit den Daten und nur den nötigsten Methoden, und freie Funktionen zur Manipulation dieser Klassen) in C++ gut erklärt?



  • Soviel gibt es da eigentlich nicht zu erklären.

    Soweit ich es verstehe, sind Klassen dafür da, zusammengehörige Daten und Funktionen vom Rest des Programm abzukapseln.

    Der Vorteil darin ist z.B. der, daß man den Programmcode in einzelne Module aufteilen kann. Diese sind übersichtlicher und leichter wartbar.



  • static_function_frage schrieb:

    Ist in C++ aber trotzdem von (expliziten) Interfaces abzuraten?

    Keine Ahnung, was ein "explizites" Interface sein soll, aber wenn du ein Interface brauchst, kannst du natürlich eines verwenden.



  • Wäre es evtl. besser, an der Stelle boost::multi_array zu verwenden?

    Ja.
    Ich zitiere mal:

    <a href= schrieb:

    Boost.Multiarray Doc">Boost MultiArray is a more efficient and convenient way to express N-dimensional arrays than existing alternatives (especially the std::vector<std::vector<...>> formulation of N-dimensional arrays).



  • Danke fürs Feedback!


  • Mod

    Also meiner Erfahrung nach ist (bzw. war vor ein paar Jahren) boost::multi_array unbrauchbar langsam. Für ein einfaches 2D-Array nimm einen Wrapper um einen 1D-vector, der die Indizes passend umbricht.



  • Ich finde die Philosophie sich boost reinzuholen für ein bisschen TicTacToe echt für Übertrieben.

    Was "static" angeht gibt es meiner Meinung keinen Grund die Routine nicht einfach in einen entsprechenden Namespace zu packen. Ich denke Pattern profitieren besser von static, indem man z.B. sowas wie Singleton implementiert.

    Alleine schon der Gedanke: ich implementiere eine static Funktion, der ich etwas über die Klasse übergebe, und ein Ergebnis bekomme, könnte man genauso ohne Parameter als echte Methode implementieren. Hingegen der Singleton wird ja vor der Existenz eines Objekts aufgerufen und erzeugt dieses.

    Oftmals wird allerdings vermittelt, dass die ganzen Methoden mit instantiiert werden und static Funktionen nur einmal existieren: Blödsinn! Mit anderen Worten man kommt ohne aus.
    Es gibt eine Hand voll Frameworks, die streng hierarchisch arbeiten und jede Klasse eine gleichnamige static Funktion implementieren muss, damit die Hierarchie funktioniert.



  • Ich finde die Philosophie[,] sich boost [für ein bisschen TicTacToe] reinzuholen[,] echt für [ü]bertrieben.

    Es geht nicht darum, für welche Größenordnungen von Projekten man Boost einbeziehen sollte.
    Boost ist - mittlerweile - zu einer Ergänzung der Standardbibliothek geworden, und wird mittlerweile auch so konsistent genutzt wie diese; es ist fast gar nicht mehr wegzudenken.
    (Vgl. mit welcher Nebenläufigkeit Boost ins Spiel kam)

    Daher sollte es für den C++-Programmierer mMn keinen bemerkenswerten Aufwand darstellen, Boost in seine Projekte einzubinden.

    Folglich kann es nicht für übertrieben gehalten werden, genau das zu tun.



  • PhilippHToner schrieb:

    Alleine schon der Gedanke: ich implementiere eine static Funktion, der ich etwas über die Klasse übergebe, und ein Ergebnis bekomme, könnte man genauso ohne Parameter als echte Methode implementieren. Hingegen der Singleton wird ja vor der Existenz eines Objekts aufgerufen und erzeugt dieses.

    Im Falle von FindWinner übergebe ich zwar auch die Membervar m_PlayGround[][]. Allerdings verwende ich diese Funktion auch für die KI, um Spielzüge und damit eben auch andere Spielfelder zu bewerten. Daher als Parameter das Spielfeld.

    Für ein einfaches 2D-Array nimm einen Wrapper um einen 1D-vector, der die Indizes passend umbricht.

    Es handelt sich ja um eine Feld mit statischer Größe Weite x Höhe, in dem Fall eben 3x3.
    Warum soll ich da überhaupt einen vector verwenden, der ja dynamisch Speicher anfordert?
    Ein Wrapper um ein 2d Array, welchem ich mit Template Parametern die Größe sowie den Typ zuweise sollte doch reichen.



  • Viel schlimmer ist doch den Namen der Klassen mit "C" zu beginnen.



  • multi_array und PlayGroundSize für ein Tic-Tac-Toe sind IMHO so sinnvoll wie
    const unsigned int days_in_week=7; für einen Kalender...



  • Muss man Tic-Tac-Toe auf 3x3 spielen?
    Muss eine Marswoche auch 7 Tage haben?



  • Swordfish schrieb:

    Muss man Tic-Tac-Toe auf 3x3 spielen?
    Muss eine Marswoche auch 7 Tage haben?

    Ein Tic-Tac-Toe hat 9 Felder. Es kann aber sicherlich auch im 8 dimensionalen gespielt werden mit einer dementsprechenden Anzahl von Feldern...
    Bei der "Woche" auf dem Mars: klär mich auf...
    Ich jedenfalls bin nicht der Meinung alles parametrisierbar machen zu müssen - Z.B. ein klassischen Tic-Tac-Toe und ein Wochenplaner für Erdzeit...


  • Mod

    Swordfish schrieb:

    Muss man Tic-Tac-Toe auf 3x3 spielen?
    Muss eine Marswoche auch 7 Tage haben?

    Bist du auch so einer der

    const int Eins = 1;
    

    macht, für den Fall, dass 1 mal seinen Wert ändern könnte? Man muss nicht immer alles bis zum letzten abstrahieren.

    Hier würde ich weder Vectorwrapper noch multiarray und erst recht nicht vector<vector> benutzen. Das sind 9 ints oder vergleichbare Datentypen! Also eine kleine(!) Compilezeitkonstante. std::array<Feldtyp, 9> oder (je nachdem, wie man es ansprechen möchte) array<array<Feldtyp, 3>, 3> würde ich als unterliegende Datentstruktur benutzen.





  • Swordfish schrieb:

    Im Kumbha stehst an: http://de.wikipedia.org/wiki/Darischer_Kalender#Aufbau_des_Kalenders 😉

    Dammit!
    Eigentlich spiele ich jeden Samstagabend Quantum Tic-Tac-Toe mit den Kumpels... 🙂



  • SeppJ schrieb:

    Bist du auch so einer [...]

    Manchmal. 😉



  • Tatsächlich habe ich nur gesagt, multi_array wäre besser als vector< vector<> > . Denn mit Multiarray kann man (neben den im Doc. genannten Vorteilen) bspw. wenigstens noch slicen und damit leichter das Feld nach Sequenzen abprüfen. 🤡

    Trotzdem ist natürlich std::array viel passender. Ich würde dann schon verschachteln, ist schließlich nur eine 3x3-Matrix.


Anmelden zum Antworten