[X] Überladung von Operatoren in C++ (Teil 2) - Einführung in boost::operators



  • Überladung von Operatoren in C++ (Teil 2) - Einführung in boost::operators

    Operatorüberladung in C++ ist ein häufig benutztes und ebenso häufig unterschätztes Feature in C++. Im ersten Teil der Artikelreihe bin ich auf die grundsätzlichen Fragen "wann?, wie?, welche?" zur Operatorüberladung eingegangen und habe zu den einzelnen überladbaren Operatoren jeweils einen Überblick zur üblichen Semantik und Implementierung im Sinne von "do as the Ints do" geliefert.

    Im vorliegenden zweiten Teil gehts um ein praktisches Hilfsmittel bei der Operatorüberladung: Die Bibliothek boost::operators unterstützt den Entwickler beim Implementieren vieler der in Teil 1 vorgestellten üblichen Praktiken. Auf den nächsten Seiten stelle ich die Gedanken und Konzepte hinter boost::operators vor und zeige anhand eines Beispiels wie man recht einfach die grundlegenden Bestandteile der Bibliothek benutzen kann. In Teil 3 der Artikelreihe gehe ich dann auf weitere Einzelheiten der Bibliothek ein und werfe einen kurzen Blick hinter die Kulissen der Implementierung.

    Voraussetzungen

    Dieser Artikel nimmt an einigen Stellen Bezug auf Aussagen im ersten Teil, dabei geht es um "best practices" bei der Operatorüberladung. Außerdem ist für die Verwendung von boost::operators ein rudimentäres Verständnis im Umgang mit Templates nötig.

    Inhalt

    Teil 2

    • 1. Ein Operator kommt selten allein
    • 1.1. Ein Beispiel: class Rational
    • 1.2. Alles Routine
    • 1.3. Arbeitserleichterung
    • 2. "Do as the ints do": Die Konzepte von boost::operators
    • 2.1. Die Operatorfamilien
    • 2.2. Arithmetische Operatorgruppen
    • 2.3. Iterator Operatoren und Iterator Helfer
    • 3. boost::operators im Einsatz
    • 3.1. Noch einmal class Rational
    • 3.2 Rational trifft boost

    1. Ein Operator kommt selten allein

    1.1. Ein Beispiel: class Rational

    Wer sich einmal eine Liste der überladbaren Operatoren anschaut, der sieht, dass es sich um rund 50 Operatoren handelt, bei denen so ziemlich jeder beliebig viele mögliche Überladungen hat. Selbst wenn man sich auf die für eine gegebene Klasse sinnvollen Operatorüberladungen beschränkt bleibt noch eine beträchtliche zahl zu implementierender Funktionen. Nun kann man sich natürlich herausreden und sagen "Wieso? Ich möchte nur 5 Operationen implementieren, also brauche ich nur 5 Operatoren." Dem ist aber nicht so.

    Nehmen wir einmal das Standardbeispiel für eine Klasse mathematischer Objekte, die Klasse Rational für Brüche. Die nötigen Operationen sind schnell aus dem Ärmel geschüttelt: vier Grundrechenarten, Vorzeichenwechsel, Test auf Gleichheit und Vergleichsrelation (kleiner als). Die Deklaration ist ebenso schnell hingeschrieben:

    class Rational
    {
    public:
      Rational operator-() const;
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs);
    Rational operator-(Rational const& lhs, Rational const& rhs);
    Rational operator*(Rational const& lhs, Rational const& rhs);
    Rational operator/(Rational const& lhs, Rational const& rhs);
    bool operator==(Rational const& lhs, Rational const& rhs);
    bool operator<(Rational const& lhs, Rational const& rhs);
    

    Voilá. Sieben Operationen, sieben Operatoren. Aber damit nicht genug, es geht gerade erst los. In Teil 1 haben wir erfahren, dass sich Operatoren verhalten müssen wie man es erwartet. Dazu gehört auch, dass man statt a = a + b auch a += b schreiben kann, statt a < b auch b > a usw. Die Operatoren haben Verwandte und bilden Operatorfamilien. Hat man einen Operator aus einer Familie, dann erwartet man die anderen auch. Also erweitern wir unsere Deklarationen:

    class Rational
    {
    public:
      Rational operator-() const;
      Rational operator+() const; //neu
    
      Rational& operator+=(Rational const& rhs); //neu
      Rational& operator-=(Rational const& rhs); //neu
      Rational& operator*=(Rational const& rhs); //neu
      Rational& operator/=(Rational const& rhs); //neu
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs);
    Rational operator-(Rational const& lhs, Rational const& rhs);
    Rational operator*(Rational const& lhs, Rational const& rhs);
    Rational operator/(Rational const& lhs, Rational const& rhs);
    bool operator==(Rational const& lhs, Rational const& rhs);
    bool operator!=(Rational const& lhs, Rational const& rhs); //neu
    bool operator<(Rational const& lhs, Rational const& rhs);
    bool operator>(Rational const& lhs, Rational const& rhs); //neu
    bool operator<=(Rational const& lhs, Rational const& rhs); //neu
    bool operator>=(Rational const& lhs, Rational const& rhs); //neu
    

    Damit sind wir schon bei 16 Operatoren. Das ist mehr Arbeit als es auf den ersten Blick ausgesehen hat.

    1.2. Alles Routine

    Wenn man sich jetzt arbeitswütig ins Getümmel stürzt und anfängt die Operatoren einen nach dem anderen zu implementieren, merkt man schnell, dass sich vieles wiederholt: die Addition in operator+ und operator+= ist die gleiche, ähnliches gilt für die anderen Grundrechenarten, und die verschiedenen Vergleichsoperatoren ähneln sich auch sehr.

    In den "üblichen Implementationen" in Teil 1 habe ich an mehreren Stellen darauf hingewiesen, dass man Operatoren mit Hilfe anderer Operatoren implementieren sollte. Wenn man einen Operator implementiert hat, kann man oft die anderen Operatoren der Familie implementieren, indem man den bereits vorhandenen aufruft. Damit reduziert sich der Aufwand für unsere Rational -Operatoren auf sechs Implementierungen und einen Haufen Einzeiler:

    class Rational
    {
    public:
      Rational operator-() const { /* IMPLEMENTIEREN */ }
      Rational operator+() const { return *this; }
    
      Rational kehrwert() const { /* IMPLEMENTIEREN */ } //fuer die Division
    
      Rational& operator+=(Rational const& rhs) { /* IMPLEMENTIEREN */ } 
      Rational& operator-=(Rational const& rhs) { return *this += -rhs; }
      Rational& operator*=(Rational const& rhs) { /* IMPLEMENTIEREN */ }
      Rational& operator/=(Rational const& rhs) { return *this *= kehrwert(rhs); }
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp += rhs; }
    Rational operator-(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp -= rhs; }
    Rational operator*(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp *= rhs; }
    Rational operator/(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp /= rhs; }
    
    bool operator==(Rational const& lhs, Rational const& rhs) { /* IMPLEMENTIEREN */ }
    bool operator!=(Rational const& lhs, Rational const& rhs) { return !(lhs == rhs); }
    bool operator<(Rational const& lhs, Rational const& rhs)  { /* IMPLEMENTIEREN */ }
    bool operator>(Rational const& lhs, Rational const& rhs)  { return rhs < lhs; }
    bool operator<=(Rational const& lhs, Rational const& rhs) { return !(rhs > lhs); }
    bool operator>=(Rational const& lhs, Rational const& rhs) { return !(rhs < lhs); }
    

    Also ist es doch alles nicht so wild. Die paar Einzeiler runden das Bild ab, sie sind schnell geschrieben und sehen eh immer gleich aus. Als Bonus kommt dazu dass sie automatisch konsistent mit den anderen Operatoren in ihrer Familie sind. Besser gehts doch garnicht, oder?
    Doch, es geht besser.

    1.3. Arbeitserleichterung

    Entwickler sind von Natur aus faul. Die Devise lautet "Tue nichts was der Computer für dich tun kann", und die Königsdisziplin heißt Automatisierung. Die oben gezeigten Einzeiler sehen immer und überall gleich aus, und wenn etwas überall gleich ist dann schreit es danach, automatisiert zu werden. Die Herren von den boost-Bibliotheken haben den Schrei gehört und antworten mit einer eigenen Bibliothek, die unserer Faulheit genüge tut und uns das lästige Tippen abnimmt. In unserem Beispiel sieht das dann so aus:

    #include <boost/operators.hpp>
    
    class Rational :  boost::ordered_field_operators<Rational>   
    {
    public:
      Rational operator-() const { /* IMPLEMENTIEREN */ }
      Rational operator+() { return *this; };
    
      Rational reziprok() const { /* IMPLEMENTIEREN */ }
    
      Rational& operator+=(Rational const& rhs) { /* IMPLEMENTIEREN */ } 
      Rational& operator-=(Rational const& rhs) { return *this += -rhs; }
      Rational& operator*=(Rational const& rhs) { /* IMPLEMENTIEREN */ }
      Rational& operator/=(Rational const& rhs) { return *this *= reziprok(rhs); }
    };
    
    bool operator==(Rational const& lhs, Rational const& rhs) { /* IMPLEMENTIEREN */ }
    bool operator<(Rational const& lhs, Rational const& rhs)  { /* IMPLEMENTIEREN */ }
    

    Alle zusätzlichen Operatoren, die wir oben mit nervigem Copy&Paste und einzelnen Änderungen an den Deklarationen hinzufügen mussten, werden durch die simple Ableitung von einem einzelnen Template hinzugeneriert. Welche Templates man braucht um bestimmte Operatoren zu generieren, und welche Voraussetzungen man dafür liefern muss, wird im folgenden Kapitel beschrieben.

    2. "Do as the ints do": Die Konzepte von boost::operators

    boost::operators ist dazu gedacht, automatisch Operatoren zu generieren, deren manuelle Implementierung immer gleich aussehen würde, weil sich die entsprechenden Klassen und Operationen so verhalten sollen wie man es von Standarddatentypen her gewohnt ist. Unter dieses erwartete Verhalten fallen pauschal gesagt quasi sämtliche Punkte, die in Teil 1 zu den einzelnen Operatoren erwähnt werden. Dazu gehören sowohl semantische Eigenheiten einzelner Operatoren wie die Kommutativität von Multiplikation und Addition als auch das Vorkommen von verwandten Operatoren. boost::operators nimmt uns also für diese "langweiligen" Operatoren fast sämtliche Arbeit ab, der Aufwand für den Entwickler beschränkt sich auf wenige Zeilen. Auf der anderen Seite bedeutet das allerdings, dass wir uns keine unüblichen oder exotischen Operatoren einfallen lassen sollten, wenn wir sie nicht wieder komplett von Hand schreiben wollen. Aber das tun wir ja sowieso nur äußerst selten, schließlich wissen wir aus Teil 1, dass unübliche und exotische Operatoren den Anwender nur verwirren und deshalb nur mit guten Gründen und einer noch besseren Dokumentation serviert werden sollten.

    2.1. Die Operatorfamilien

    Boost definiert für die verschiedenen Operatorfamilien jeweils ein oder mehrere Templates. Pro Familie muss ein "Basisoperator" definiert sein, dessen Verhalten von den anderen Operatoren der Familie übernommen wird und der vom Entwickler der Klasse definiert werden muss. Damit die Operatorfamilien generiert werden können, müssen die Basisoperatoren bestimmte Bedingungen erfüllen, z.B. müssen die Ergebnistypen der Vergleichsoperatoren in einen bool konvertierbar sein. Die Operatorfamilien und die zugehörigen Basisoperatoren sowie eventuell nötige weitere Bedingungen werden in der folgenden Tabelle aufgelistet:

    Die Familien der üblichen arithmetischen und bitweisen Operatoren sind selbsterklärend. Zwei weitere Operatorfamilien, dereferencable und indexable , generieren Pointer/Iterator-Operatoren: mit dereferencable wird mittels operator* ein operator-> erzeugt, und indexable erzeugt einen operator[] , so dass ptr[n] == *(ptr + n) .

    Die verschiedenen Operatorfamilien werden weiter zusammengefasst zu Gruppen. Dabei unterscheidet Boost zwischen den arithmetischen Operatorgruppen und den iteratorbezogenen Operatorgruppen. Es steht dem Benutzer frei, die angebotenen Operatorgruppen zu verwenden oder seine Klasse von mehreren Operatorfamilien abzuleiten; bei modernen Compilern mit den heutzutage üblichen Features wie Empty Base Class Optimization ist das Ergebnis identisch.

    2.2. Arithmetische Operatorgruppen

    Meistens ist für einen bestimmten Typ mehr als ein Operator (bzw. eine Operatorfamilie) sinnvoll. Die Addition geht meist Hand in Hand mit der Subtraktion, für Zahlentypen wie Rational werden gleich alle vier Grundrechenarten benötigt usw.

    Die einzelnen Familien der arithmetischen Operatoren werden daher in Gruppen zusammengefasst, für die jeweils eigene Templates definiert sind. Die Gruppe ordered_field_operators enthält z.B. die Familien addable , subtractable , multiplicable , dividable , less_than_comparable und equality_comparable - die Namen sprechen für sich. Die verschiedenen arithmetischen Operatorgruppen sowie die Familien, die sie umfassen, werden im Folgenden kurz dargestellt.

    Bei den mathematischen Operatoren gibt es manchmal zwei Gruppen mit den selben Familien, aber verschiedenen Namen. Dies beruht auf den verschiedenen möglichen Betrachtungsweisen der Operatorgruppen: eine einfache Zusammenfassung der Grundrechenarten auf der einen Seite, mathematisch- gruppentheoretische Überlegungen auf der anderen Seite. Die Operatorgruppen der Grundrechenarten sind additive und multiplicative , in denen jeweils die Familien addable und subtractable bzw. multipliable und dividable zusammengefasst sind. Diese beiden Gruppen ergeben zusammengefasst die Gruppe arithmetic (d.h. die 4 Grundrechenarten). Dazu gibt es noch die Gruppen integer_multipliable und integer_arithmetic , wo den entsprechenden Gruppen noch die Modulo-Operation hinzugefügt wurde ( modable ).

    Die gruppentheoretische Seite sieht wie folgt aus: additive und multipliable , also die Familien um +,- und *, ergeben die Gruppe ring_operators . Zusammen mit der Division erhält man die field_operators , mit Division und Modulo die euclidian_ring_operators . Die Vergleichsfamilien less_than_comparable und equality_comparable ergeben zusammen die Gruppe totally_ordered . Fügt man diese wiederum den einzelnen gruppentheoretischen Operatorgruppen hinzu, so ergeben sich ordered_ring_operators , ordered_field_operators (siehe das Beispiel für Rational oben) und ordered_euclidian_ring_operators .

    Zusätzlich zu all dem gibt es noch drei weitere kleinere arithmetische Operatorgruppen: bitwise setzt sich aus den drei Familien für bitweise Operationen zusammen (&, | und ^), unit_steppable umfasst die Inkrement- und Dekrement-Familien und shiftable wie der Name schon sagt die beiden Shifts.

    2.3. Iterator Operatoren und Iterator Heler

    Ähnlich wie bei den arithmetischen Gruppen gibt es Operatorgruppen, die die üblichen Operationen der verschiedenen Iteratorarten umfassen, wie sie auch im C++98 Standard, §24.1 definiert sind. Der Name ist jeweils Programm: input_iterable , output_iterable , forward_iterable , bidirectional_iterable und random_access_iterable . input_iterable und forward_iterable beinhalten dabei beide lediglich die Inkrementoperatoren, die Namen lassen aber darauf schließen in welchem Kontext die jeweiligen Iteratorklassen verwendet werden sollen.

    Zusätzlich zu den Operatorgruppen für Iteratoren gibt es noch jeweils einen sogenannten Iterator Helper, der zusätzlich zu den geerbten Operatorgruppen noch die vom Standard verlangten typedefs für die jeweilige Iteratorart beinhaltet. Der Helper für Input-Iteratoren heißt input_iterator_helper , für die vier anderen Iteratorarten werden die Namen ähnlich gebildet.

    3. boost::operators im Einsatz

    Im Folgenden wird die grundlegende Verwendung von boost::operators an Hand unserer Rational -Klasse genauer gezeigt.

    3.1. Noch einmal class Rational

    Gehen wir es also nochmal gründlich an mit unserer Klasse für rationale Zahlen:

    • Wir nehmen für die interne Darstellung das, was am Nächsten liegt, nämlich zwei ints für Zähler und Nenner.
    • Destruktor und Zuweisungsoperator interessieren uns nicht weiter, ebensowenig der Copy-Ctor, da hier die compilergenerierten ausreichend sind.
    • Weitere Konstruktoren die wir brauchen könnten sind ein Konstruktor fur die explizite Angabe von Zähler und Nenner, ein Defaultkonstruktor, der wie bei ints und anderen Standardtypen nullinitialisiert sowie einen Konvertierungskonstruktor von int nach Rational.
    • Eine Konvertierung von double nach Rational sehen wir wegen der unterschiedlichen Wertebereiche nicht vor, allerdings statten wir unsere Klasse mit einer Konvertirungsfunktion nach double aus (keinen Konvertierungsoperator, um später Mehrdeutigkeiten zu vermeiden).
    • Schließlich nehmen wir noch an, dass wir eine Kürzungsfunktion haben, die in fast jeder Operation aufgerufen wird und dafür sorgt, dass Zähler und Nenner so weit wie möglich gekürzt sind. Eine weitere Invariante soll sein, dass nur der Zähler vorzeichenbehaftet ist.

    Die Behandlung von Division durch Null (sowohl bei der Rechenoperation als auch wenn der Zähler Null ist) und von Integerüberläufen werde ich hier vorerst nicht behandeln.

    class Rational
    {
      //Invarianten:
      //- zaehler und nenner sind immer vollstaendig gekuerzt
      //- der nenner ist immer positiv (Vorzeichen befindet sich am zaehler)
      int zaehler;
      int nenner;
    
      void kuerzen(); 
    
    public:
      //Konstruktoren:
      //Default- und Konvertierungs-Konstruktor für ints gleich mit eingebaut...
      Rational(int z = 0, int n = 1) 
        : zaehler(n>0?z:-z), 
          nenner(n>0?n:-n) 
      { 
        kuerzen(); 
      } 
    
      //Copy-Ctor compilergeneriert
      //Destruktor compilergeneriert
      //Zuweisung für Rational compilergeneriert
      //Zuweisung für int implizit generiert durch Konvertierungs-Konstruktor
    
      //Vorzeichen:
      Rational operator- () const 
      {
        Rational tmp(*this);
        tmp.zaehler *= -1;
        return tmp;
      }
    
      Rational operator+ () const 
      { 
        return *this; 
      }
    
      //Umwandlungsfunktionen:
      Rational kehrwert() const
      {
        Rational tmp(nenner, zaehler);
        return tmp;
      }
    
      double toDouble() const
      {
        return static_cast<double>(zaehler)/nenner;
      }
    };
    

    Als nächstes kommt die Implementierung der vier Grundrechenarten. Wie man der Tabelle aus Kapitel 2.1. entnehmen kann, braucht boost::operators dafür die Operatoren +=, -= usw. Außerdem kann man nach Vorlage der Standardtypen double und float die Inkrement- und Dekrementoperatoren so implementieren, dass sie jeweils eine Erhöhung/Erniedrigung um 1 bedeuten. Was noch bleibt sind die Vergleichsoperatoren:

    class Rational
    {
      /* ... s.o. ...*/
    public:
    
      //Grundrechenarten
      Rational& operator+= (Rational const& rhs)
      {
        zaehler *= rhs.nenner;
        zaehler += nenner*rhs.zaehler;
        nenner *= rhs.nenner;
        kuerzen();
        return *this;
      }
      Rational& operator-= (Rational const& rhs)
      {
        return operator+=(-rhs);
      }
    
      Rationa& operator*= (Rational const& rhs)
      {
        zaehler *= rhs.zaehler;
        nenner *= rhs.nenner;
        kuerzen();
        return *this;
      }
    
      Rational& operator/= (Rational const& rhs)
      {
        return operator*=(rhs.kehrwert());
      }  
    
      //Inkrement, Dekrement
      Rational& operator++()
      {
        zaehler += nenner;
        return *this;
      }
      Rational& operator--()
      {
        zaehler -= nenner;
        return *this;
      }
    
      //Vergleich, als freie friend-Funktion
      friend bool operator< (Rational const& lhs, Rational const& rhs)
      { 
        return lhs.zaehler*rhs.nenner < rhs.zaehler*lhs.nenner; 
      }
    };
    

    Damit hätten wir das Grundgerüst schon so weit, dass wir den Rest von Boost erledigen lassen können.

    3.2. Rational trifft boost

    Schauen wir uns nochmal die Tabelle der Operatorfamilien an und vergleichen sie mit dem, was wir unserer Klasse schon mitgegeben haben. Folgende Familien können (und sollten) wir damit benutzen:

    • Die Grundrechenarten, also addable , subtractable , multipliable und dividable .
    • incrementable und decrementable
    • less_than_comparable , equivalent und dadurch equality_comparable

    Um unsere Klasse jeder einzelnen dieser Familien bekannt zu machen haben wir zwei Möglichkeiten, nämlich einmal indem Rational direkt von jeder einzelnen erbt und einmal indem wir eine Vererbungskette aufbauen mit einer Technik, die boost base class chaining nennt. Die Vererbung darf je nach Laune public, protected oder private geschehen, das hat keinen Einfluss aufs Resultat.

    //Mehrfachvererbung, flache Hierarchie:
    class Rational : boost::addable<Rational>, boost::subtractable<Rational>, boost::multipliable<Rational>, boost::dividable<Rational>, 
                     boost::incrementable<Rational>, boost::decrementable<Rational>, 
                     boost::less_than_comparable<Rational>, boost::equivalent<Rational>, boost::equality_comparable<Rational>
    {
      /*...*/
    };
    
    //base class chaining:
    class Rational : boost::addable<Rational
                   , boost::subtractable<Rational
                   , boost::multipliable<Rational
                   , boost::dividable<Rational
                   , boost::incrementable<Rational
                   , boost::decrementable<Rational
                   , boost::less_than_comparable<Rational
                   , boost::equivalent<Rational
                   , boost::equality_comparable<Rational> > > > > > > > >
    {
      /*...*/
    };
    

    Das sieht beides recht wüst aus. In der ersten Version haben wir eine neunfach-Vererbung, in der zweiten Version ein neunfach geschachteltes Template. All die Operatorfamilien-Templates haben einen optionalen zusätzlichen Parameter, der als Basisklasse dient. Die oberste Klasse in der erzeugten Hierarchie ist also die equality_comparable -Familie, die vorletzte die addable -Familie. Die Technik des base class chaining ist relativ neu und in älteren Versionen der Bibliothek nicht enthalten. Die Gründe warum sie eingeführt wurde werden in Teil 3 der Artikelreihe erläutert, es wird trotz der etwas schwierigeren Schreibweise empfohlen sie an Stelle der Mehrfachvererbung zu benutzen.

    Wie schon erwähnt hat boost das Konzept der Operatorgruppen. Damit lässt sich der große Haufen Templates um einiges reduzieren:

    //base class chaining mit Operatorgruppen
    class Rational : boost::ordered_field_operators<Rational  //Operatoren +, -, *, /, >, >=, <=, !=
                   , boost::unit_steppable<Rational           //Postinkrement und -dekrement
                   , boost::equivalent<Rational> > >          //operator==
    {
      /*...*/
    };
    

    Mit den drei Zeilen werden also mal eben 11 zusätzliche Operatoren generiert, besser gehts kaum! Damit haben wir alles, um rationale Zahlen mit anderen rationalen Zahlen zu verrechnen und zu vergleichen. Da wir den Konvertierungskonstruktor für int haben und boost freundlicherweise alle nötigen binären Operatoren als freie Funktionen liefert, haben wir frei Haus auch die Grundrechenarten und Vergleiche mit ints auf alle erdenkliche Arten mitgeliefert bekommen.

    Fazit und Ausblick

    Wie man sieht kann Boost uns hier wiedereinmal viel Arbeit abnehmen. Mit geringem Aufwand können die eigenen Klassen mit einem vollständigen Satz von Operatoren ausgestattet werden.

    Im folgenden Artikel werde ich die Unterstützung von gemischten Operatoren durch boost::operators erläutern und die Klasse Rational um gemischte Rechenoperationen mit double erweitern. Außerdem zeige ich die Verwendung der Iterator Helfer am Beispiel eines simplen Array-Iterators und werfe anschließend einen Blick in die Implementation von boost::operators.

    Quellen

    Der fertige Quellcode der Rational-Klasse inklusive Behandlung der Nulldivision (eine einfache Exception) kann hier heruntergeladen werden.
    Der vorgestellte Code wurde auf MSVC 2008 kompiliert und getestet (Tippfehler vorbehalten)



  • Kapitel 1 und 2 sind soweit fertig und können gerne bereits kommentiert werden 🙂



  • Teil 3.2 ist fertig (class Rational), ich bitte um Tests obs portabel ist (getestet auf MSVC 2008), sowie Rückmeldungen 🙂



  • void kuerzen() { /* Ein wenig Primzahlmagie */ }
    

    Es lässt sich darüber streiten ob der Euklidische Algorithmus Primzahlmagie ist oder nicht. Das einfachste wäre wohl den Algo einfach beim Namen zu nennen.

    zaehler(n>0?z:-z)
    

    -INT_MIN ist meistens 0. Schöner wäre es ein int und ein unsigned als Parameter zu nehmen. Da kann so etwas dann nicht passieren.

    Assginment

    Zuweisungsoperator oder operator= klingt IMHO besser.

    Rational tmp(nenner, zaehler);
        return tmp;
    

    kannst du zu

    return Rational(nenner, zaehler);
    

    vereinfachen

    Du hast einiges an Makros in deinem Rational drin. Ich finde dies unschön, denn wenn man schon Makros verwendet, dann kann man auch gleich alle Operatoren damit erzeugen.

    Diese BOOST_NS Sachen sind auch scheinbar nirgendwo erklärt.



  • Ben04 schrieb:

    zaehler(n>0?z:-z)
    

    -INT_MIN ist meistens 0. Schöner wäre es ein int und ein unsigned als Parameter zu nehmen. Da kann so etwas dann nicht passieren.

    Dafür kann jemand einen Rational (1,-2) konstruieren und sich drüber wundern dass stattdessen ein 1/4294967294 draus geworden ist. Das kommt vermutlich häufiger vor als der 1/(-INT_MIN) Versuch, der eine Exception wirft und daher schneller zu lokalisieren ist als obskure Rechenfehler.



  • Das Beispiel für den iterator ist auch fertig fürs erste 🙂
    Nächster Schritt noch ein paar Details zur Template-Spielerei im boost::operators Header, danach geh ich nochmal komplett drüber.



  • Sodele, ausgehend von der Diskussion bzgl. überlanger Artikel hab ich mir einen Punkt gesucht, an dem man den Artikel teilen könnte:

    In Abschnitt 3.1, nach der ersten Verheiratung von Rational mit boost::operators.

    Die Aufteilung wäre dan etwa wie folgt:

    • Teil 2: Einführung in boost::operators

    • Operatoren sind Rudeltiere 😉

    • Konzepte von boost::operators (Operatorfamilien, und -gruppen)

    • Rational trifft boost::operators - Benutzung der einfachsten Operatortemplates

    • Teil 3: boost::operators für Fortgeschrittene

    • Rational mit gemischten Operatoren

    • class myvector<T>::iterator

    • Blick hinter die Kulissen

    Kurzfristiger Feedback zu der Aufteilung wäre wünschenswert, dann kann ich den so geplanten Teil2 zum nächsten Termin überarbeiten und fertig stellen.



  • Damit genügend Zeit für die technische und orthographische Revision bleibt hab ich die Aufteilung mal kurzerhand umgesetzt und bin nochmal drübergegangen.
    Daher also [T] für diesen Teil der Artikelreihe 🙂



  • Die Aufteilung sieht gut aus. 🙂



  • Ben04 schrieb:

    Die Aufteilung sieht gut aus. 🙂

    Gut 🙂
    Falls keine weiteren Einwände kommen und keine technischen Verbesserungen vorzunehmen sind setze ich die Sache am Freitag auf [R], damits noch diesen Monat raus kann 🙂



  • Ich werd's mir morgen noch als (unbedarfter) Leser anschauen 😉
    RS-Korrektur mach ich dann am WE.



  • GPC schrieb:

    Ich werd's mir morgen noch als (unbedarfter) Leser anschauen 😉

    Inwiefern unbedarft? O.O



  • pumuckl schrieb:

    GPC schrieb:

    Ich werd's mir morgen noch als (unbedarfter) Leser anschauen 😉

    Inwiefern unbedarft? O.O

    Ich habe recht wenig Ahnung von boost, daher "unbedarft"



  • GPC schrieb:

    Ich habe recht wenig Ahnung von boost, daher "unbedarft"

    Dann kannst du aber umso besser beurteilen obs der Zielgruppe, die mit boost::operators nocht nichts zu tun hatte, angemesen ist 😉



  • pumuckl schrieb:

    GPC schrieb:

    Ich habe recht wenig Ahnung von boost, daher "unbedarft"

    Dann kannst du aber umso besser beurteilen obs der Zielgruppe, die mit boost::operators nocht nichts zu tun hatte, angemesen ist 😉

    Jo, ebend 😉

    Der Artikel ließt sich flüssig, es ist alles gut erklärt und leicht verständlich. Gefällt mir! 🙂 Nur eine Frage... warum hast du die Opfamilien dereferencable und indexable nicht auch in die Tabelle mitaufgenommen?

    RS-Korrektur mach ich wie gesagt über's WE.



  • GPC schrieb:

    Nur eine Frage... warum hast du die Opfamilien dereferencable und indexable nicht auch in die Tabelle mitaufgenommen?

    Ich wusste ich hatte beim Überarbeiten was übersehen... Done.



  • Überladung von Operatoren in C++ (Teil 2) - Einführung in boost::operators

    Operatorüberladung in C++ ist ein häufig benutztes und ebenso häufig unterschätztes Feature in C++. Im ersten Teil der Artikelreihe bin ich auf die grundsätzlichen Fragen "wann?, wie?, welche?" zur Operatorüberladung eingegangen und habe zu den einzelnen überladbaren Operatoren jeweils einen Überblick zur üblichen Semantik und Implementierung im Sinne von "Do as the ints do" geliefert.

    Im vorliegenden zweiten Teil geht es um ein praktisches Hilfsmittel bei der Operatorüberladung: Die Bibliothek boost::operators unterstützt den Entwickler beim Implementieren vieler der in Teil 1 vorgestellten üblichen Praktiken. Auf den nächsten Seiten stelle ich die Gedanken und Konzepte hinter boost::operators vor und zeige anhand eines Beispiels wie man recht einfach die grundlegenden Bestandteile der Bibliothek benutzen kann. In Teil 3 der Artikelreihe gehe ich dann auf weitere Einzelheiten der Bibliothek ein und werfe einen kurzen Blick hinter die Kulissen der Implementierung.

    Voraussetzungen

    Dieser Artikel nimmt an einigen Stellen Bezug auf Aussagen im ersten Teil, dabei geht es um "best practices" bei der Operatorüberladung. Außerdem ist für die Verwendung von boost::operators ein rudimentäres Verständnis im Umgang mit Templates nötig.

    Inhalt

    Teil 2

    • 1 Ein Operator kommt selten allein
    • 1.1 Ein Beispiel: class Rational
    • 1.2 Alles Routine
    • 1.3 Arbeitserleichterung
    • 2 "Do as the ints do": Die Konzepte von boost::operators
    • 2.1 Die Operatorfamilien
    • 2.2 Arithmetische Operatorgruppen
    • 2.3 Iterator Operatoren und Iterator Helfer
    • 3 boost::operators im Einsatz
    • 3.1 Noch einmal class Rational
    • 3.2 Rational trifft boost

    1 Ein Operator kommt selten allein

    1.1 Ein Beispiel: class Rational

    Wer sich einmal eine Liste der überladbaren Operatoren anschaut, der sieht dass es sich um rund 50 Operatoren handelt, bei denen so ziemlich jeder beliebig viele mögliche Überladungen hat. Selbst wenn man sich auf die für eine gegebene Klasse sinnvollen Operatorüberladungen beschränkt bleibt noch eine beträchtliche zahl zu implementierender Funktionen. Nun kann man sich natürlich herausreden und sagen "Wieso? Ich möchte nur 5 Operationen implementieren, also brauche ich nur 5 Operatoren." Dem ist aber nicht so.

    Nehmen wir einmal das Standardbeispiel für eine Klasse mathematischer Objekte, die Klasse Rational für Brüche. Die nötigen Operationen sind schnell aus dem Ärmel geschüttelt: vier Grundrechenarten, Vorzeichenwechsel, Test auf Gleichheit und Vergleichsrelation (kleiner als). Die Deklaration ist ebenso schnell hingeschrieben:

    class Rational
    {
    public:
      Rational operator-() const;
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs);
    Rational operator-(Rational const& lhs, Rational const& rhs);
    Rational operator*(Rational const& lhs, Rational const& rhs);
    Rational operator/(Rational const& lhs, Rational const& rhs);
    bool operator==(Rational const& lhs, Rational const& rhs);
    bool operator<(Rational const& lhs, Rational const& rhs);
    

    Voilá. Sieben Operationen, sieben Operatoren. Aber damit nicht genug, es geht gerade erst los. In Teil 1 haben wir erfahren, dass sich Operatoren verhalten müssen wie man es erwartet. Dazu gehört auch, dass man statt a = a + b auch a += b schreiben kann, statt a < b auch b > a usw. Die Operatoren haben Verwandte und bilden Operatorfamilien. Hat man einen Operator aus einer Familie, dann erwartet man die anderen auch. Also erweitern wir unsere Deklarationen:

    class Rational
    {
    public:
      Rational operator-() const;
      Rational operator+() const; //neu
    
      Rational& operator+=(Rational const& rhs); //neu
      Rational& operator-=(Rational const& rhs); //neu
      Rational& operator*=(Rational const& rhs); //neu
      Rational& operator/=(Rational const& rhs); //neu
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs);
    Rational operator-(Rational const& lhs, Rational const& rhs);
    Rational operator*(Rational const& lhs, Rational const& rhs);
    Rational operator/(Rational const& lhs, Rational const& rhs);
    bool operator==(Rational const& lhs, Rational const& rhs);
    bool operator!=(Rational const& lhs, Rational const& rhs); //neu
    bool operator<(Rational const& lhs, Rational const& rhs);
    bool operator>(Rational const& lhs, Rational const& rhs); //neu
    bool operator<=(Rational const& lhs, Rational const& rhs); //neu
    bool operator>=(Rational const& lhs, Rational const& rhs); //neu
    

    Damit sind wir schon bei 16 Operatoren. Das ist mehr Arbeit als es auf den ersten Blick ausgesehen hat.

    1.2 Alles Routine

    Wenn man sich jetzt arbeitswütig ins Getümmel stürzt und anfängt die Operatoren einen nach dem anderen zu implementieren, merkt man schnell, dass sich vieles wiederholt: die Addition in operator+ und operator+= ist die gleiche, ähnliches gilt für die anderen Grundrechenarten und die verschiedenen Vergleichsoperatoren ähneln sich auch sehr.

    In den "üblichen Implementationen" in Teil 1 habe ich an mehreren Stellen darauf hingewiesen, dass man Operatoren mit Hilfe anderer Operatoren implementieren sollte. Wenn man einen Operator implementiert hat, kann man oft die anderen Operatoren der Familie implementieren, indem man den bereits vorhandenen aufruft. Damit reduziert sich der Aufwand für unsere Rational -Operatoren auf sechs Implementierungen und einen Haufen Einzeiler:

    class Rational
    {
    public:
      Rational operator-() const { /* IMPLEMENTIEREN */ }
      Rational operator+() const { return *this; }
    
      Rational kehrwert() const { /* IMPLEMENTIEREN */ } //fuer die Division
    
      Rational& operator+=(Rational const& rhs) { /* IMPLEMENTIEREN */ } 
      Rational& operator-=(Rational const& rhs) { return *this += -rhs; }
      Rational& operator*=(Rational const& rhs) { /* IMPLEMENTIEREN */ }
      Rational& operator/=(Rational const& rhs) { return *this *= kehrwert(rhs); }
    };
    
    Rational operator+(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp += rhs; }
    Rational operator-(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp -= rhs; }
    Rational operator*(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp *= rhs; }
    Rational operator/(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp /= rhs; }
    
    bool operator==(Rational const& lhs, Rational const& rhs) { /* IMPLEMENTIEREN */ }
    bool operator!=(Rational const& lhs, Rational const& rhs) { return !(lhs == rhs); }
    bool operator<(Rational const& lhs, Rational const& rhs)  { /* IMPLEMENTIEREN */ }
    bool operator>(Rational const& lhs, Rational const& rhs)  { return rhs < lhs; }
    bool operator<=(Rational const& lhs, Rational const& rhs) { return !(rhs > lhs); }
    bool operator>=(Rational const& lhs, Rational const& rhs) { return !(rhs < lhs); }
    

    Also ist es doch nicht so wild. Die paar Einzeiler runden das Bild ab, sie sind schnell geschrieben und sehen sowieso immer gleich aus. Als Bonus kommt dazu dass sie automatisch konsistent mit den anderen Operatoren in ihrer Familie sind. Besser gehts doch garnicht, oder?
    Doch, es geht besser.

    1.3 Arbeitserleichterung

    Entwickler sind von Natur aus faul. Die Devise lautet "Tue nichts was der Computer für dich tun kann" und die Königsdisziplin heißt Automatisierung. Die oben gezeigten Einzeiler sehen immer und überall gleich aus und wenn etwas überall gleich ist, dann schreit es danach automatisiert zu werden. Die Herren von den boost-Bibliotheken haben den Schrei gehört und antworten mit einer eigenen Bibliothek, die unserer Faulheit genüge tut und uns das lästige Tippen abnimmt. In unserem Beispiel sieht das dann so aus:

    #include <boost/operators.hpp>
    
    class Rational :  boost::ordered_field_operators<Rational>   
    {
    public:
      Rational operator-() const { /* IMPLEMENTIEREN */ }
      Rational operator+() { return *this; };
    
      Rational reziprok() const { /* IMPLEMENTIEREN */ }
    
      Rational& operator+=(Rational const& rhs) { /* IMPLEMENTIEREN */ } 
      Rational& operator-=(Rational const& rhs) { return *this += -rhs; }
      Rational& operator*=(Rational const& rhs) { /* IMPLEMENTIEREN */ }
      Rational& operator/=(Rational const& rhs) { return *this *= reziprok(rhs); }
    };
    
    bool operator==(Rational const& lhs, Rational const& rhs) { /* IMPLEMENTIEREN */ }
    bool operator<(Rational const& lhs, Rational const& rhs)  { /* IMPLEMENTIEREN */ }
    

    Alle zusätzlichen Operatoren, die wir oben mit nervigem Copy&Paste und einzelnen Änderungen an den Deklarationen hinzufügen mussten, werden durch die simple Ableitung von einem einzelnen Template hinzugeneriert. Welche Templates man braucht um bestimmte Operatoren zu generieren und welche Voraussetzungen man dafür liefern muss wird im folgenden Kapitel beschrieben.

    2 "Do as the ints do": Die Konzepte von boost::operators

    boost::operators ist dazu gedacht automatisch Operatoren zu generieren, deren manuelle Implementierung immer gleich aussehen würde, weil sich die entsprechenden Klassen und Operationen so verhalten sollen wie man es von Standarddatentypen her gewohnt ist. Unter dieses erwartete Verhalten fallen pauschal gesagt quasi sämtliche Punkte, die in Teil 1 zu den einzelnen Operatoren erwähnt werden. Dazu gehören sowohl semantische Eigenheiten einzelner Operatoren wie die Kommutativität von Multiplikation und Addition als auch das Vorkommen von verwandten Operatoren. boost::operators nimmt uns also für diese "langweiligen" Operatoren fast sämtliche Arbeit ab, der Aufwand für den Entwickler beschränkt sich auf wenige Zeilen. Auf der anderen Seite bedeutet das allerdings, dass wir uns keine unüblichen oder exotischen Operatoren einfallen lassen sollten, wenn wir sie nicht wieder komplett von Hand schreiben wollen. Aber das tun wir ja sowieso nur äußerst selten, schließlich wissen wir aus Teil 1, dass unübliche und exotische Operatoren den Anwender nur verwirren und deshalb nur mit guten Gründen und einer noch besseren Dokumentation serviert werden sollten.

    2.1 Die Operatorfamilien

    Boost definiert für die verschiedenen Operatorfamilien jeweils ein oder mehrere Templates. Pro Familie muss ein "Basisoperator" definiert sein, dessen Verhalten von den anderen Operatoren der Familie übernommen wird und der vom Entwickler der Klasse definiert werden muss. Damit die Operatorfamilien generiert werden können, müssen die Basisoperatoren bestimmte Bedingungen erfüllen, z.B. müssen die Ergebnistypen der Vergleichsoperatoren in einen bool konvertierbar sein. Die Operatorfamilien und die zugehörigen Basisoperatoren sowie eventuell nötige weitere Bedingungen werden in der folgenden Tabelle aufgelistet:

    Die Familien der üblichen arithmetischen und bitweisen Operatoren sind selbsterklärend. Zwei weitere Operatorfamilien, dereferencable und indexable , generieren Pointer/Iterator-Operatoren: mit dereferencable wird mittels operator* ein operator-> erzeugt und indexable erzeugt einen operator[] , so dass ptr[n] == *(ptr + n) .

    Die verschiedenen Operatorfamilien werden weiter zusammengefasst zu Gruppen. Dabei unterscheidet Boost zwischen den arithmetischen Operatorgruppen und den iteratorbezogenen Operatorgruppen. Es steht dem Benutzer frei, die angebotenen Operatorgruppen zu verwenden oder seine Klasse von mehreren Operatorfamilien abzuleiten; bei modernen Compilern mit den heutzutage üblichen Features wie Empty Base Class Optimization ist das Ergebnis identisch.

    2.2 Arithmetische Operatorgruppen

    Meistens ist für einen bestimmten Typ mehr als ein Operator (bzw. eine Operatorfamilie) sinnvoll. Die Addition geht meist Hand in Hand mit der Subtraktion, für Zahlentypen wie Rational werden gleich alle vier Grundrechenarten benötigt usw.

    Die einzelnen Familien der arithmetischen Operatoren werden daher in Gruppen zusammengefasst, für die jeweils eigene Templates definiert sind. Die Gruppe ordered_field_operators enthält z.B. die Familien addable , subtractable , multiplicable , dividable , less_than_comparable und equality_comparable - die Namen sprechen für sich. Die verschiedenen arithmetischen Operatorgruppen sowie die Familien, die sie umfassen, werden im Folgenden kurz dargestellt.

    Bei den mathematischen Operatoren gibt es manchmal zwei Gruppen mit den selben Familien, aber verschiedenen Namen. Dies beruht auf den verschiedenen möglichen Betrachtungsweisen der Operatorgruppen: eine einfache Zusammenfassung der Grundrechenarten auf der einen Seite, mathematisch- gruppentheoretische Überlegungen auf der anderen Seite. Die Operatorgruppen der Grundrechenarten sind additive und multiplicative , in denen jeweils die Familien addable und subtractable bzw. multipliable und dividable zusammengefasst sind. Diese beiden Gruppen ergeben zusammengefasst die Gruppe arithmetic (d.h. die 4 Grundrechenarten). Dazu gibt es noch die Gruppen integer_multipliable und integer_arithmetic , wo den entsprechenden Gruppen noch die Modulo-Operation hinzugefügt wurde ( modable ).

    Die gruppentheoretische Seite sieht wie folgt aus: additive und multipliable , also die Familien um +,- und *, ergeben die Gruppe ring_operators . Zusammen mit der Division erhält man die field_operators , mit Division und Modulo die euclidian_ring_operators . Die Vergleichsfamilien less_than_comparable und equality_comparable ergeben zusammen die Gruppe totally_ordered . Fügt man diese wiederum den einzelnen gruppentheoretischen Operatorgruppen hinzu, so ergeben sich ordered_ring_operators , ordered_field_operators (siehe das Beispiel für Rational oben) und ordered_euclidian_ring_operators .

    Zusätzlich zu all dem gibt es noch drei weitere kleinere arithmetische Operatorgruppen: bitwise setzt sich aus den drei Familien für bitweise Operationen zusammen (&, | und ^), unit_steppable umfasst die Inkrement- und Dekrement-Familien und shiftable - wie der Name schon sagt - die beiden Shifts.

    2.3 Iterator Operatoren und Iterator Heler

    Ähnlich wie bei den arithmetischen Gruppen gibt es Operatorgruppen, die die üblichen Operationen der verschiedenen Iteratorarten umfassen, wie sie auch im C++98 Standard, §24.1 definiert sind. Der Name ist jeweils Programm: input_iterable , output_iterable , forward_iterable , bidirectional_iterable und random_access_iterable . input_iterable und forward_iterable beinhalten dabei beide lediglich die Inkrementoperatoren, die Namen lassen aber darauf schließen in welchem Kontext die jeweiligen Iteratorklassen verwendet werden sollen.

    Zusätzlich zu den Operatorgruppen für Iteratoren gibt es noch jeweils einen sogenannten Iterator Helper, der zusätzlich zu den geerbten Operatorgruppen noch die vom Standard verlangten typedefs für die jeweilige Iteratorart beinhaltet. Der Helper für Input-Iteratoren heißt input_iterator_helper , für die vier anderen Iteratorarten werden die Namen ähnlich gebildet.

    3 boost::operators im Einsatz

    Im Folgenden wird die grundlegende Verwendung von boost::operators anhand unserer Rational -Klasse genauer gezeigt.

    3.1 Noch einmal class Rational

    Gehen wir es also nochmal gründlich an mit unserer Klasse für rationale Zahlen:

    • Wir nehmen für die interne Darstellung das, was am Nächsten liegt, nämlich zwei ints für Zähler und Nenner.
    • Destruktor und Zuweisungsoperator interessieren uns nicht weiter, ebensowenig der Copy-Ctor, da hier die compilergenerierten ausreichend sind.
    • Weitere Konstruktoren die wir brauchen könnten sind ein Konstruktor fur die explizite Angabe von Zähler und Nenner, ein Defaultkonstruktor, der wie bei ints und anderen Standardtypen nullinitialisiert sowie einen Konvertierungskonstruktor von int nach Rational.
    • Eine Konvertierung von double nach Rational sehen wir wegen der unterschiedlichen Wertebereiche nicht vor, allerdings statten wir unsere Klasse mit einer Konvertirungsfunktion nach double aus (keinen Konvertierungsoperator, um später Mehrdeutigkeiten zu vermeiden).
    • Schließlich nehmen wir noch an, dass wir eine Kürzungsfunktion haben, die in fast jeder Operation aufgerufen wird und dafür sorgt, dass Zähler und Nenner so weit wie möglich gekürzt sind. Eine weitere Invariante soll sein, dass nur der Zähler vorzeichenbehaftet ist.

    Die Behandlung von Division durch Null (sowohl bei der Rechenoperation als auch wenn der Zähler Null ist) und von Integerüberläufen werde ich hier vorerst nicht behandeln.

    class Rational
    {
      //Invarianten:
      //- zaehler und nenner sind immer vollstaendig gekuerzt
      //- der nenner ist immer positiv (Vorzeichen befindet sich am zaehler)
      int zaehler;
      int nenner;
    
      void kuerzen(); 
    
    public:
      //Konstruktoren:
      //Default- und Konvertierungs-Konstruktor für ints gleich mit eingebaut...
      Rational(int z = 0, int n = 1) 
        : zaehler(n>0?z:-z), 
          nenner(n>0?n:-n) 
      { 
        kuerzen(); 
      } 
    
      //Copy-Ctor compilergeneriert
      //Destruktor compilergeneriert
      //Zuweisung für Rational compilergeneriert
      //Zuweisung für int implizit generiert durch Konvertierungs-Konstruktor
    
      //Vorzeichen:
      Rational operator- () const 
      {
        Rational tmp(*this);
        tmp.zaehler *= -1;
        return tmp;
      }
    
      Rational operator+ () const 
      { 
        return *this; 
      }
    
      //Umwandlungsfunktionen:
      Rational kehrwert() const
      {
        Rational tmp(nenner, zaehler);
        return tmp;
      }
    
      double toDouble() const
      {
        return static_cast<double>(zaehler)/nenner;
      }
    };
    

    Als nächstes kommt die Implementierung der vier Grundrechenarten. Wie man der Tabelle aus Kapitel 2.1 entnehmen kann, braucht boost::operators dafür die Operatoren +=, -= usw. Außerdem kann man nach Vorlage der Standardtypen double und float die Inkrement- und Dekrementoperatoren so implementieren, dass sie jeweils eine Erhöhung/Erniedrigung um 1 bedeuten. Was noch bleibt sind die Vergleichsoperatoren:

    class Rational
    {
      /* ... s.o. ...*/
    public:
    
      //Grundrechenarten
      Rational& operator+= (Rational const& rhs)
      {
        zaehler *= rhs.nenner;
        zaehler += nenner*rhs.zaehler;
        nenner *= rhs.nenner;
        kuerzen();
        return *this;
      }
      Rational& operator-= (Rational const& rhs)
      {
        return operator+=(-rhs);
      }
    
      Rationa& operator*= (Rational const& rhs)
      {
        zaehler *= rhs.zaehler;
        nenner *= rhs.nenner;
        kuerzen();
        return *this;
      }
    
      Rational& operator/= (Rational const& rhs)
      {
        return operator*=(rhs.kehrwert());
      }  
    
      //Inkrement, Dekrement
      Rational& operator++()
      {
        zaehler += nenner;
        return *this;
      }
      Rational& operator--()
      {
        zaehler -= nenner;
        return *this;
      }
    
      //Vergleich, als freie friend-Funktion
      friend bool operator< (Rational const& lhs, Rational const& rhs)
      { 
        return lhs.zaehler*rhs.nenner < rhs.zaehler*lhs.nenner; 
      }
    };
    

    Damit hätten wir das Grundgerüst schon so weit, dass wir den Rest von Boost erledigen lassen können.

    3.2 Rational trifft boost

    Schauen wir uns nochmal die Tabelle der Operatorfamilien an und vergleichen sie mit dem, was wir unserer Klasse schon mitgegeben haben. Folgende Familien können (und sollten) wir damit benutzen:

    • Die Grundrechenarten, also addable , subtractable , multipliable und dividable .
    • incrementable und decrementable
    • less_than_comparable , equivalent und dadurch equality_comparable

    Um unsere Klasse jeder einzelnen dieser Familien bekannt zu machen haben wir zwei Möglichkeiten, nämlich einmal indem Rational direkt von jeder einzelnen erbt und einmal indem wir eine Vererbungskette aufbauen mit einer Technik, die boost base class chaining nennt. Die Vererbung darf je nach Laune public, protected oder private geschehen, das hat keinen Einfluss auf das Resultat.

    //Mehrfachvererbung, flache Hierarchie:
    class Rational : boost::addable<Rational>, boost::subtractable<Rational>, boost::multipliable<Rational>, boost::dividable<Rational>, 
                     boost::incrementable<Rational>, boost::decrementable<Rational>, 
                     boost::less_than_comparable<Rational>, boost::equivalent<Rational>, boost::equality_comparable<Rational>
    {
      /*...*/
    };
    
    //base class chaining:
    class Rational : boost::addable<Rational
                   , boost::subtractable<Rational
                   , boost::multipliable<Rational
                   , boost::dividable<Rational
                   , boost::incrementable<Rational
                   , boost::decrementable<Rational
                   , boost::less_than_comparable<Rational
                   , boost::equivalent<Rational
                   , boost::equality_comparable<Rational> > > > > > > > >
    {
      /*...*/
    };
    

    Das sieht beides recht wüst aus. In der ersten Version haben wir eine neunfach-Vererbung, in der zweiten Version ein neunfach geschachteltes Template. All die Operatorfamilien-Templates haben einen optionalen zusätzlichen Parameter, der als Basisklasse dient. Die oberste Klasse in der erzeugten Hierarchie ist also die equality_comparable -Familie, die vorletzte die addable -Familie. Die Technik des base class chaining ist relativ neu und in älteren Versionen der Bibliothek nicht enthalten. Die Gründe warum sie eingeführt wurde werden in Teil 3 der Artikelreihe erläutert, es wird trotz der etwas schwierigeren Schreibweise empfohlen sie an Stelle der Mehrfachvererbung zu benutzen.

    Wie schon erwähnt hat boost das Konzept der Operatorgruppen. Damit lässt sich der große Haufen Templates um einiges reduzieren:

    //base class chaining mit Operatorgruppen
    class Rational : boost::ordered_field_operators<Rational  //Operatoren +, -, *, /, >, >=, <=, !=
                   , boost::unit_steppable<Rational           //Postinkrement und -dekrement
                   , boost::equivalent<Rational> > >          //operator==
    {
      /*...*/
    };
    

    Mit den drei Zeilen werden also mal eben 11 zusätzliche Operatoren generiert, besser geht es kaum! Damit haben wir alles, um rationale Zahlen mit anderen rationalen Zahlen zu verrechnen und zu vergleichen. Da wir den Konvertierungskonstruktor für int haben und boost freundlicherweise alle nötigen binären Operatoren als freie Funktionen liefert, haben wir frei Haus auch die Grundrechenarten und Vergleiche mit ints auf alle erdenkliche Arten mitgeliefert bekommen.

    Fazit und Ausblick

    Wie man sieht kann Boost uns hier wiedereinmal viel Arbeit abnehmen. Mit geringem Aufwand können die eigenen Klassen mit einem vollständigen Satz von Operatoren ausgestattet werden.

    Im nächten Artikel dieser Reihe werde ich die Unterstützung von gemischten Operatoren durch boost::operators erläutern und die Klasse Rational um gemischte Rechenoperationen mit double erweitern. Außerdem zeige ich die Verwendung der Iterator Helfer am Beispiel eines simplen Array-Iterators und werfe anschließend einen Blick in die Implementation von boost::operators.

    Quellen

    Der fertige Quellcode der Rational-Klasse inklusive Behandlung der Nulldivision (eine einfache Exception) kann hier heruntergeladen werden.
    Der vorgestellte Code wurde auf MSVC 2008 kompiliert und getestet (Tippfehler vorbehalten)



  • Okay, RS-Korrektur ist erledigt. War eigentlich nicht viel zu tun... ein paar Kommafehler und dass man bei der Nummerierung der Kapitel keinen Punkt nach der letzten Ziffer platziert.



  • Vielen Dank 🙂



  • Dieser Thread wurde von Moderator/in GPC aus dem Forum Die Redaktion in das Forum Archiv verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.


Anmelden zum Antworten