Dieser und noch weitere Artikel wurde von pumuckl erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


Ü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

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:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
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:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 !(lhs > rhs); }
bool operator>=(Rational const& lhs, Rational const& rhs) { return !(lhs < rhs); }

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:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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: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.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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: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.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//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:
C++:
//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)

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign