Wrapper-Klassen



  • Hallo!

    Habe das Thema unter folgendem Link schonmal angeschnitten und interessante Lösungsansätze bekommen.

    http://www.c-plusplus.net/forum/viewtopic.php?p=1806722#1806722

    Danke.

    Jetzt bin ich soweit voran gekommen, dass sich das Problem ganz massiv stellt. Ich besitze sehr viele Funktionen in meiner Engine und zudem sind die Parameterlisten meistens zwischen 2-4 Parametern des gleichen Typs (Integer) lang.

    Beispiel:

    int init_evasions(const board_c & board, int piece, int color, int square);
    int gen_captures(const board_c & board, int color, int file);
    

    Das ist relativ fehleranfällig, da die Parameterlisten ähnlich sind und mal ganz schnell beim Aufrufen einer Funktion die Parameter vertauscht werden können ohne, dass es vom Compiler festgestellt werden kann!

    Möchte ich diese zusätzliche Fehlerquelle abschalten, muss ich die verschiedenen Integers in den Parameterlisten entweder

    1. in eine Wrapper-Klasse packen oder
    2. eine Enumeration mit überladenen Operatoren einführen.

    Option 2. gefällt mir gar nicht, auch deshalb, weil ich die arithmetischen Operatoren nicht überladen möchte. Auch wurde mir hier im Forum gesagt, dass Enumertor-Typen nicht zum Rechnen gemacht sind?!

    Daher bleibt nur die Wrapper-Klasse. Das habe ich erstmal rudimentär wie folgt implemetiert:

    struct color_t {
      int a;
    };
    
    struct piece_t {
      int a;
    };
    
    struct square_t {
      int a;
    };
    
    struct file_t {
      int a;
    };
    
    struct rank_t {
      int a;
    };
    
    struct bitboard_t {
      uint64_t a;
    };
    

    Und schon sehen meine Funktionsdeklarationen von oben wie folgt aus:

    int init_move(const board_c & board, piece_t piece, color_t color, square_t square);
    int gen_captures(const board_c & board, color_t color, file_t file);
    

    Der Compiler ist zufrieden und macht mich auf jeden kleine Nachlässigkeit aufmerksam. Meine Frage ist jetzt:

    Ist dieser Ansatz vernünftig oder ist das nicht state-of-the-art des Programmierens, Integers in kleine Wrapper-Klassen (Strukturen) zu verpacken und diese dann zu benutzen und damit zu rechnen?

    Danke für eure Kommentare!



  • Tomahawk schrieb:

    Auch wurde mir hier im Forum gesagt, dass Enumertor-Typen nicht zum Rechnen gemacht sind?!

    Das war ich wohl damals. Wieso "?!", kannst du das nicht nachvollziehen? 😉

    Tomahawk schrieb:

    Ist dieser Ansatz vernünftig oder ist das nicht state-of-the-art des Programmierens, Integers in kleine Wrapper-Klassen (Strukturen) zu verpacken und diese dann zu benutzen und damit zu rechnen?

    Der Ansatz kann durchaus vernünftig sein. Ich würde allerdings nicht Const-Referenzen verwenden, da die Klassen sehr klein sind. Bei int nimmst du ja auch Kopien als Parameter.

    Ich würde aber gerne Genaueres über die einzelnen Typen erfahren. Wieso meinst du, für alle Operatorüberladung bereitstellen zu müssen? Inwiefern muss eine Farbe, eine Datei oder ein Stück (was immer piece_t bezeichnet) Operatoren unterstützen? Denke daran: Operatoren sollte man nur in Kontexten überladen, in denen sie der erwarteten Semantik entsprechen. Sonst sind Funktionen mit aussagekräftigen Bezeichnern besser geeignet.

    Kannst du die einzelnen Typen etwas mehr erläutern (wofür sie stehen, welche Zustände sie haben können etc.)? Das muss nicht sehr ins Detail gehen, aber dann könnte man besser abschätzen, ob dein Vorgehen gerechtfertigt ist. Ich habe nämlich den Verdacht, dass enum in einigen Fällen sinnvoll wäre, Operatorüberladung hingegen nicht.



  • Dazu habe ich auch mal 'ne Frage (etwas OT, aber nur etwas 😉 )
    Sind opaque typedefs eigentlich noch für C++0x vorgesehen? Das wäre ja quasi genau das, was hier benötigt wird.
    Das neuste Dokument im Netz, wo etwas darüber gesagt wird, ist von 2006. Zumindest kann ich nichts aktuelleres finden. Weiß jemand mehr?



  • Wegen der Const Referenzen:

    Der Parameter

    board_c & board
    

    ist tatsächlich ein relativ grosses Objekt und muss bei mir immer übergeben werden, da ich mit mehreren Threads arbeite und es deshalb auch eine Instanz für jeden Thread gibt. Aber das ist hier nicht das Thema 😉

    Zu den Typen:

    file_t ist eine Linie auf dem Schachbrett: 0 <= file <= 7, Additionen und Subtraktionen sind erwünscht.

    rank_t ist eine Reihe auf dem Schachbrett: wie file_t

    square_t ist ein Feld auf dem Schachbrett: 0 <= square <= 63, Additionen und Subtraktionen sind erwünscht. Ausserdem Shifts, OR und AND (<< >> | &).

    color_t ist die Farbe der aktiven Seite (während der Suche): 0 <= color <= 1: Hier wird auch gerechnet, um die Farbe zu tauschen zB: color ^= 1 und so weiter...

    Fazit:

    Ausser mit piece_t (Schachfigur: Bauer bis König (0 <= piece <= 5)) wird mit allen Typen gerechnet und mit piece_t möchte ich mir evtl die Option offen halten auch rechnen zu können.

    🙂



  • Tomahawk schrieb:

    file_t ist eine Linie auf dem Schachbrett: 0 <= file <= 7, Additionen und Subtraktionen sind erwünscht.

    rank_t ist eine Reihe auf dem Schachbrett: wie file_t

    Okay. Hier siehst du einen blanken int wahrscheinlich wegen der Typsicherheit als ungeeignet an (wobei diese auch irgendwo Grenzen haben muss). Wenn du das wirklich durchziehen willst, solltest du einen weiteren Typen offset_t oder so einrichten, der Differenzen von Linien/Reihen speichert. Denn es macht keinen Sinn, zwei Reihen zu addieren, hingegen kann Subtraktion erwünscht sein. Viele Operationen eines integralen Datentyps brauchst du auch gar nicht. Du kannst natürlich auch rank_offset_t und file_offset_t definieren. 😉

    Das gibt zwar etwas Aufwand, aber z.B. mit Boost.Operators ist auch die Operatorüberladung kein Problem mehr (Artikel dazu). Ach ja, und ich würde vielleicht gerade unsigned -Typen nehmen (evtl. auch unsigned short ), da werden Dinge wie assert für Wertprüfungen vereinfacht.

    Tomahawk schrieb:

    square_t ist ein Feld auf dem Schachbrett: 0 <= square <= 63, Additionen und Subtraktionen sind erwünscht. Ausserdem Shifts, OR und AND (<< >> | &).

    Wieso brauchst du einen Typen fürs Feld? Durch die Reihe und Zeile ist doch dieses eindeutig bestimmt. Ich würde eher eine Struktur mit diesen beiden Eigenschaften als Feldpositions-Datentyp einrichten.

    Ansonsten verstehe ich auch nicht, was du mit diesen Bitoperatoren genau willst. Wären entsprechende Nicht-Operator-Funktionen keine Alternative?

    Tomahawk schrieb:

    color_t ist die Farbe der aktiven Seite (während der Suche): 0 <= color <= 1: Hier wird auch gerechnet, um die Farbe zu tauschen zB: color ^= 1 und so weiter...

    Da nur zwei Zustände vorkommen, könnte man eigentlich bool nehmen. Allerdings scheint mir ein enum angebrachter. Statt rohen Operatoren, die in diesem Zusammenhang nicht besonders klar sind (wieso soll man eine Farbe XORen?), empfehle ich wiederum Funktionen.

    enum color_t
    {
        white,
        black,
    };
    
    void toggle_color(color_t& color);
    

    Tomahawk schrieb:

    Ausser mit piece_t (Schachfigur: Bauer bis König (0 <= piece <= 5)) wird mit allen Typen gerechnet und mit piece_t möchte ich mir evtl die Option offen halten auch rechnen zu können.

    Wieso solltest du hier rechnen können? Das wäre ein klassischer Fall für ein enum : Eine genau definierte Anzahl von Zuständen, welche aussagekräftige Namen haben und als gemeinsamen Typ angesprochen werden können.

    Wie du siehst, halte ich es für wichtig, dass du etwas abstrahierst und von dem Low-Level-Bitschieberei wegkommst. Das heisst, richte für Operationen, die du mit einem Namen bezeichnen kannst, Funktionen ein. Es wird auch für dich leichter, wenn du eine Aktion gleich erkennst, als wenn du zuerst Bit für Bit nachvollziehen musst -- besonders, nachdem du den Code eine Zeit nicht mehr angeschaut hast. Die Low-Level-Operationen hast du nach wie vor -- nur sind sie entsprechend gekapselt und treten im Anwendungscode kaum noch auf. Das erleichtert auch allfällige Änderungen.



  • Das gibt zwar etwas Aufwand, aber z.B. mit Boost.Operators ist auch die Operatorüberladung kein Problem mehr (Artikel dazu). Ach ja, und ich würde vielleicht gerade unsigned -Typen nehmen (evtl. auch unsigned short ), da werden Dinge wie assert für Wertprüfungen vereinfacht.

    Das mit dem Einsatz von unsigned Integers überlege ich schon länger. Da meine Werte (bis auf die Stellungsevaluation) immer positiv sind.

    Wieso brauchst du einen Typen fürs Feld? Durch die Reihe und Zeile ist doch dieses eindeutig bestimmt. Ich würde eher eine Struktur mit diesen beiden Eigenschaften als Feldpositions-Datentyp einrichten.

    Es wäre einfach zu langsam ein Feld über Linie und Reihe zu definieren. Dann würde ich ja einen 2-dim Vektor benötigen. Nach meiner Erfahrung verkompliziert das die Berechnungen. Ganz grob: 90% aller brettrelevanten Berechnungen sind feld-spezifisch. die übrigen 10% verteilen sich auf Linien und Reihen.

    Ansonsten verstehe ich auch nicht, was du mit diesen Bitoperatoren genau willst. Wären entsprechende Nicht-Operator-Funktionen keine Alternative?

    Bitoperationen benötige ich auf dem Brett eine Menge, hier ein Ausschnitt:

    // header file
    
    inline int square_to_file(const int square) {
      return square & 7;
    }
    
    inline int square_to_rank(const int square) {
      return square >> 3;
    }
    
    inline int relative_square(const int color, const int square) {
      return square ^ (color * 56);
    }
    
    inline int flip_square(const int square) {
      return square ^ 56;
    }
    
    inline int file_and_rank_to_square(const int file, const int rank) {
      return file + (rank << 3);
    }
    
    inline int relative_rank(const int color, const int rank)  {
      return rank ^ (color * 7);
    }
    
    inline int flip_rank(const int rank) {
      return rank ^ 7;
    }
    

    Da nur zwei Zustände vorkommen, könnte man eigentlich bool nehmen. Allerdings scheint mir ein enum angebrachter. Statt rohen Operatoren, die in diesem Zusammenhang nicht besonders klar sind (wieso soll man eine Farbe XORen?), empfehle ich wiederum Funktionen.

    Wie kann ich einen Bool ganz schnell umkehren? Und ausserdem kann ich einen Bool meines Wissen nicht als Arrayindex einsetzen. COLOR_WHITE und COLOR_BLACK werden auch zur Indexierung von Tabellen verwendet:

    // source file
    
    int king_sq[COLORS];
    int piece_ct[COLORS][PIECES];
    
    // header file
    
    const int COLOR_WHITE = 0;
    const int COLOR_BLACK = 1;
    const int COLOR_NONE = 2;
    
    const int COLORS = 2;
    
    inline int opposite_color(const int color) {
      return color ^ 1;
    }
    

    Wieso solltest du hier rechnen können? Das wäre ein klassischer Fall für ein enum : Eine genau definierte Anzahl von Zuständen, welche aussagekräftige Namen haben und als gemeinsamen Typ angesprochen werden können.

    Stimmt, hier wird tatsächlich nicht gerechnet, habe ich übersehen 😉

    Wie du siehst, halte ich es für wichtig, dass du etwas abstrahierst und von dem Low-Level-Bitschieberei wegkommst. Das heisst, richte für Operationen, die du mit einem Namen bezeichnen kannst, Funktionen ein.

    Auf jeden Fall wird (fast) alles in kleine inlines verpackt, das Ausdenken von aussagekräftigen Namen dauert manchmal länger als mir lieb ist.

    Es wird auch für dich leichter, wenn du eine Aktion gleich erkennst, als wenn du zuerst Bit für Bit nachvollziehen musst -- besonders, nachdem du den Code eine Zeit nicht mehr angeschaut hast.

    Ja. Hier folgt ein typisches Beispiel wie ich im Augenblick verschachtele und verzweifelt versuche die Bitfummeleien etwas zu abstrahieren:

    // header
    
    inline int move_to(const int move) {
      return move & 63;
    }
    
    inline int move_is_promotion(const int move) {
      return move & IS_PROMOTION;
    }
    
    inline int move_is_enpassant(const int move) {
      return move & IS_ENPASSANT;
    }
    
    inline int board_c::piece_on_square(const int square) const {
      return piece_on[square];
    }
    
    inline bool board_c::move_is_capture(const int move) const {
      return (piece_on_square(move_to(move)) != PIECE_NONE) || move_is_enpassant(move);
    }
    
    inline bool board_c::move_is_capture_or_promotion(const int move) const {
      return move_is_capture(move) || move_is_promotion(move);
    }
    

    Danke!



  • Nur nochmal als kleine Frage:

    Öfters wurde hier im Forum kommentiert, dass der const -Bezeichner in meinen Parameterlisten unsinnig sei. Da ich erst seit kurzem programmiere und mir das selbst beibringe, weiss ich tatsächlich nicht, ob es schlecher Stil ist, das so zu tun?

    Versuche eben, alle Daten, die von ihrer Bestimmung nicht veränderbar sein sollen als const zu verwalten. Bei der Übergabe von Objekten via Reference oder Pointer ist mir das schon klar, dort muss man das ja quasi machen.

    Ist es bei Integers überflüssig und unsinnig mit const zu arbeiten?

    (siehe Beispiel der Parameterlisten in meinem letzten Posting weiter oben...)

    Danke!



  • Tomahawk schrieb:

    Nur nochmal als kleine Frage:

    Öfters wurde hier im Forum kommentiert, dass der const -Bezeichner in meinen Parameterlisten unsinnig sei. Da ich erst seit kurzem programmiere und mir das selbst beibringe, weiss ich tatsächlich nicht, ob es schlecher Stil ist, das so zu tun?

    Versuche eben, alle Daten, die von ihrer Bestimmung nicht veränderbar sein sollen als const zu verwalten. Bei der Übergabe von Objekten via Reference oder Pointer ist mir das schon klar, dort muss man das ja quasi machen.

    Ist es bei Integers überflüssig und unsinnig mit const zu arbeiten?

    (siehe Beispiel der Parameterlisten in meinem letzten Posting weiter oben...)

    Danke!

    Ja, es ist überflüssig, da du by value übergibst, in der Funktion also mit einer Kopie arbeitest, die am Ende der Funktion ohnehin verworfen wird. Eine Änderung würde sich also lediglich innerhalb der Funktion auswirken.

    Stefan.



  • Tomahawk schrieb:

    Es wäre einfach zu langsam ein Feld über Linie und Reihe zu definieren. Dann würde ich ja einen 2-dim Vektor benötigen. Nach meiner Erfahrung verkompliziert das die Berechnungen. Ganz grob: 90% aller brettrelevanten Berechnungen sind feld-spezifisch. die übrigen 10% verteilen sich auf Linien und Reihen.

    Bei Annahmen über Geschwindigkeit wäre ich immer vorsichtig. Wenn du häufig Linien/Reihen und Felder ineinander umrechnen musst, kann das schliesslich auch etwas ausmachen. Du sagst ja, dass du noch nicht allzu lange programmierst, und Anfänger neigen dazu, Code wegen vermeintlicher Geschwindigkeitsgewinne umständlich zu machen (Premature Optimization). Kann aber auch gut sein, dass du Recht hast. Versuch, den Code einigermassen benutzerfreundlich und wartbar zu halten, und im Zweifelsfalle misst du eben die Zeit.

    Tomahawk schrieb:

    Bitoperationen benötige ich auf dem Brett eine Menge, hier ein Ausschnitt:
    [...]
    Auf jeden Fall wird (fast) alles in kleine inlines verpackt, das Ausdenken von aussagekräftigen Namen dauert manchmal länger als mir lieb ist.

    Das ist sehr gut! 👍
    Ich dachte eben, du würdest die bitweisen Operatoren dauernd im Anwendungscode einsetzen, aber wenn du sie durch Funktionen verständlich machst, ist das kein Problem.

    Tomahawk schrieb:

    Wie kann ich einen Bool ganz schnell umkehren?

    Gleich wie die anderen integralen Typen:

    bool b = true;
    b ^= true; // true statt 1 (spielt eigentlich keine Rolle)
    

    Tomahawk schrieb:

    Und ausserdem kann ich einen Bool meines Wissen nicht als Arrayindex einsetzen.

    Kannst du schon, bool ist wie gesagt ein integraler Typ, der eben nur zwei Werte annehmen kann: false und true (bzw. in Zahlen ausgedrückt 0 und 1). Du könntest also höchstens ein Array mit zwei Elementen indizieren.

    Tomahawk schrieb:

    COLOR_WHITE und COLOR_BLACK werden auch zur Indexierung von Tabellen verwendet:

    // source file
    
    int king_sq[COLORS];
    int piece_ct[COLORS][PIECES];
    
    // header file
    
    const int COLOR_WHITE = 0;
    const int COLOR_BLACK = 1;
    const int COLOR_NONE = 2;
    
    const int COLORS = 2;
    
    inline int opposite_color(const int color) {
      return color ^ 1;
    }
    

    Eben, nimm dafür enum . Enumerator-Konstanten können auch als integrale Zahlen interpretiert werden.

    enum color_t
    {
        WHITE,   // erster Enumerator entspricht implizit dem integralen Wert 0
        BLACK,   // entspricht automatisch dem Vorgänger+1, also 0+1 == 1
        NO_COLOR // entspricht automatisch 1+1 == 2
    };
    
    // Du kannst die Zahlen auch explizit angeben,
    // das ist genau das gleiche [c]enum[/c]:
    enum color_t
    {
        WHITE = 0,
        BLACK = 1,
        NO_COLOR = 2
    };
    
    const int NUM_COLORS = 2; // NUM zum Verdeutlichen, dass es sich um eine Anzahl handelt
    

    Der Vorteil besteht darin, dass du einen eigenen Typen für Farben hast und nicht mit int hantieren musst.

    color_t a = BLACK;
    color_t b = 1; // Fehler
    

    Umgang mit Arrays:

    int king_sq[NUM_COLORS];
    king_sq[WHITE] = ...;
    king_sq[BLACK] = ...;
    

    Umkehrfunktion ( static_cast braucht hier keine Rechenzeit, sondern dient nur zur Umgehung der Typsicherheit):

    inline color_t opposite_color(color_t color)
    {
      return static_cast<color_t>(static_cast<int>(color) ^ 1);
    }
    

    Tomahawk schrieb:

    Öfters wurde hier im Forum kommentiert, dass der const -Bezeichner in meinen Parameterlisten unsinnig sei.

    Das ist in der Tat so, wie DStefan schon sagte. Du kopierst den Wert sowieso. Ob du nun die Kopie innerhalb der Funktion verändern darfst, geht die Schnittstelle nichts an. Es wird auch in der Funktion niemand daran gehindert, eine weitere Kopie zu erstellen, welche verändert werden kann. Kurz gesagt ist const bei Value-Parametern absolut sinnlos. Ausserdem tragen unnötige Schlüsselwörter dazu bei, dass relevante Dinge weniger gut erkennbar sind. Ich denke bei const in Parameterlisten z.B. immer an Zeiger oder Referenzen auf const , also dass ein Argument von der Funktion nicht direkt verändert wird. Das ist bei einer Kopie aber sowieso schon gegeben.

    Glaube auch nicht, möglichst viel const führe zwangsläufig zu gutem und sicherem Code. Setze Const-Correctness ein, wo sie sinnvoll ist. 🙂


Anmelden zum Antworten