Ü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 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
aucha += b
schreiben kann, statta < b
auchb > 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+
undoperator+=
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 !(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:
#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
undindexable
, generieren Pointer/Iterator-Operatoren: mitdereferencable
wird mittelsoperator*
einoperator->
erzeugt undindexable
erzeugt einenoperator[]
, so dassptr[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 Familienaddable
,subtractable
,multiplicable
,dividable
,less_than_comparable
undequality_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
undmultiplicative
, in denen jeweils die Familienaddable
undsubtractable
bzw.multipliable
unddividable
zusammengefasst sind. Diese beiden Gruppen ergeben zusammengefasst die Gruppearithmetic
(d.h. die 4 Grundrechenarten). Dazu gibt es noch die Gruppeninteger_multipliable
undinteger_arithmetic
, wo den entsprechenden Gruppen noch die Modulo-Operation hinzugefügt wurde (modable
).Die gruppentheoretische Seite sieht wie folgt aus:
additive
undmultipliable
, also die Familien um +,- und *, ergeben die Gruppering_operators
. Zusammen mit der Division erhält man diefield_operators
, mit Division und Modulo dieeuclidian_ring_operators
. Die Vergleichsfamilienless_than_comparable
undequality_comparable
ergeben zusammen die Gruppetotally_ordered
. Fügt man diese wiederum den einzelnen gruppentheoretischen Operatorgruppen hinzu, so ergeben sichordered_ring_operators
,ordered_field_operators
(siehe das Beispiel fürRational
oben) undordered_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 undshiftable
- 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
undrandom_access_iterable
.input_iterable
undforward_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
unddividable
. incrementable
unddecrementable
less_than_comparable
,equivalent
und dadurchequality_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 dieaddable
-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
- Dokumentation zu boost::operators: http://www.boost.org/doc/libs/1_39_0/libs/utility/operators.htm
- Überladung von Operatoren in C++ (Teil 1): http://magazin.c-plusplus.net/artikel/�berladung von Operatoren in CPlusPlus (Teil 1)
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)
-
Danke, guter Artikel
-
Guter Artikel!
Eine Anmerkung hab ich dann doch noch:
pumuckl schrieb:
Rational operator+(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); return tmp += rhs; }
...ist nicht so gut wie...
Rational operator+(Rational const& lhs, Rational const& rhs) { Rational tmp(lhs); tmp += rhs; return tmp;}
Warum? Nun, "tmp+=rhs" gibt eine Referenz zurück. Im ersten Fall "return tmp+=rhs;" ist zur Compile-Zeit nicht klar, was für eine Referenz das ist -- es sei denn der Compiler ist superschlau und stellt dies beim Inlining von operator+= fest. Im zweiten Fall taucht in der Return-Anweisung das lokale Objekt "tmp" auf. Hier kann der Compiler die "[N]amed [R]eturn [V]alue [O]ptimization" (NRVO) durchführen.
Der GCC C++ Compiler führt die NRVO im zweiten Fall durch. Im ersten Fall nicht -- auch nicht mit höchster Optimierungsstufe. Man kann sich also mit der zweiten Version eine Kopie eines Rational-Objektes sparen.
Gruß,
Sebastian
-
Hallo Sebastian, danke für die Anmerkung
Das war allerdings nur ein Beispiel, später übernimmt boost ja die Implementierung der Operatoren. Die sieht dann im Falle eines NRVO-fähigen Compilers, oder wenn das Flag BOOST_FORCE_SYMMETRIC_OPERATORS gesetzt ist (wird im nächsten Artikel angesprochen), so aus, wie du vorgeschlagen hast, ansonsten sinngemäßRational operator+ (Rational lhs, Rational const& rhs) { return lhs += rhs; }