todo
Version: 3
Hinweis - dieser Artikel ist noch nicht vollständig - Kapitel 3 fehlt teilweise und Kapitel 4 noch komplett. Ein Ausschnitt des Gesamt-Artikel entspricht dem Vortrag, den ich am 16.05.2012 auf dem C++ User-Treffen in Düsseldorf gehalten habe.
Inhaltsverzeichnis
- 1. Einführung
- 2. Grundlagen
-
3. Operator-Besonderheiten
- 3.1 Unäre Operatoren
- 3.2 Zuweisungs-Operator ("=")
- 3.3 Operative-Zuweisungs-Operatoren (z.B. "+=")
- 3.4 Prä- und Post-Increment und Decrement Operatoren ("++" und "--")
- 3.5 Logisches "Und" ("&&") und logisches "Oder" ("||")
- 3.6 Adress-Operator ("&")
- 3.7 Array-Zugriff bzw. Index-Operator ("[ ]")
- 3.8 Element-Zugriff bzw. Pfeil-Operator ("->")
- 3.9 Zeiger-Auf-Element-Zugriff bzw. Pfeil-Stern-Operator ("->*")
- 3.10 Funktions-Aufruf-Operatoren ("( )")
- 3.11 Konvertierungs-Operatoren
- 3.12 User-definierte Literale - Operator ("""")
- 4. Boost.Operator
- 5. Fazit
- 6. Links
- 7. Literatur
- 8. Versions-Historie
1. Einführung
1.1 Operator-Überladung
-
„Lehnen Sie sich zurück und entspannen Sie sich“
- Denn Operator-Überladung ist eigentlich ganz einfach
-
Warum dann dieser Artikel?
- Viele C++ Programmierer tun sich schwer damit
- Operator-Überladung hat das Stigma des Mysthischen & Undurchschaubaren
- Der Artikel soll zeigen, dass Operator-Überladung ganz einfach ist...
-
Operator-Überladung
-
Wird auch genannt
- "Operator-Funktionen"
- "Operator-Funktions-Überladung"
- Denn in C++ sind Operatoren Funktionen
-
Und damit ist schon vieles gesagt
- Funktionen können überladen werden
- => Operatoren können auch überladen werden
- Man muss nur ein paar Besonderheiten beachten
-
Wird auch genannt
-
Typische Motivation
- Mathematische Klasse, z.B. für Brüche
-
Man möchte mathematische Funktionen lesbar hinschreiben
- Zum Beispiel für Brüche - Klasse "Rational"
- Rational r1, r2, r3, r4;
- r1 = r2 * r3 + 6 * r4 + 1;
-
Aber in C++ sind andere Dinge viel wichtiger
- Streams
- Mengen-Klassen wie Container oder auch Strings
- Smart-Pointer
- Iteratoren
- Funktions-Objekte
- DSEL (Domain-Specific-Embedded-Language)
-
Deklaration & Definiton
- Wie Funktionen
- Nur mit dem Schlüsselwort "operator" und dem Operator selber als Funktions-Namen
-
Es muss mindestens ein Operand ein benutzer-definierter Typ sein
-
Klassen-Typ (d.h. Kopie, L-Value-Referenz oder R-Value-Referenz) oder Enum
"Zeiger-auf-Klasse" reicht nicht aus! - Als Element-Funktion ist die Klasse selber ("*this") schon der benutzer-definierte Typ
-
Klassen-Typ (d.h. Kopie, L-Value-Referenz oder R-Value-Referenz) oder Enum
-
Besonderheiten
-
Der Aufruf ist möglich
- Sowohl in Funktions-Schreibweise
- Als auch in Operator-Schreibweise
-
Priorität
- Die Priorität der Operatoren bleiben bestehen
-
Priorität
- Welcher Operator wird vor welchem ausgewertet?
- Beispiel: Punkt-Rechnung vor Strich-Rechnung
- Richtig: 2+3*4 <=> 2+(3*4) <=> 2+12 <=> 14
- Und nicht: 2+3*4 <=> (2+3)*4 <=> 5*4 <=> 20
-
Auswertungs-Reihenfolge
- Auch Assozativität genannt
- Die Auswertungs-Reihenfolge der Operatoren bleiben bestehen
-
Auswertungs-Reihenfolge:
- Werden die Operatoren von "links nach rechts" (L=>R) oder von "rechts nach links" (R=>L) ausgewertet?
- Beispiel: Minus-Rechnung
- Richtig: 8-3-1 <=> (8-3)-1 <=> 5-1 <=> 4
- Und nicht: 8-3-1 <=> 8-(3-1) <=> 8-2 <=> 6
-
Es können keine neuen Operatoren definiert werden
- Keine neuen Operatoren möglich wie z.B. ** für das Potenzieren
-
Die Anzahl an Operanden liegt fest
- Binäres + (Addition) hat zwei Operanden: a+b
- Ausnahme: Funktions-Aufruf-Operator ()
-
Default-Argumente sind nicht möglich
- Ausnahme: Funktions-Aufruf-Operator ()
-
Operatoren können überladen werden als:
- Als Element-Funktionen – alle außer "new/delete/new[]/..."
- Als Klassen-Funktionen – keine außer "new/delete/new[]/..."
- Als freie Funktionen – die Meisten, aber nicht Alle
-
Operator-Überladung benötigt immer min. einen benutzer-definierten Typ
-
Benutzer-definierte Typen:
- Enums
-
Klassen bzw. Strukturen - als Kopie, L-Value-Referenz oder R-Value-Referenz
"Zeiger-auf-Klasse" reicht nicht aus!
-
In der Sprache vorhandene Operatoren können nicht neu definiert werden
- „int + int“ ist vorhanden und läßt sich nicht ändern
-
Benutzer-definierte Typen:
-
Nicht alle Operatoren können überladen werden
-
Es lassen sich 46 der 57 C++ Operatoren überladen
- Zusätzlich können noch Konvertierungs-Operatoren, benutzer-definierte Literale (C++11) und eigene "new/delete/new[]/..." Varianten erzeugt werden
- Die Operatoren "new/delete/new[]/..." für die dynamische Speicherverwaltung, sind die einzigen Operatoren, die sich in einer Klasse nicht als Element-Funktion, sondern nur als Klassen-Funktion überladen lassen. Zusätzlich steht die Überladung der globalen Operatoren "new/delete/new[]/..." als freie Funktionen zur Verfügung.
-
Folgende 11 Operatoren sind die Ausnahmen
- Bereichszuordnung "::"
- Komponentenzugriff "."
- Komponente über Komponentenzeiger ".*"
- Bedingter Ausdruck "? :"
- "sizeof"
- "typeid"
- Klassischer C-Cast
- Cast-Operator: "static_cast"
- Cast-Operator: "const_cast"
- Cast-Operator: "reinterpret_cast"
- Cast-Operator: "dynamic_cast"
-
Es lassen sich 46 der 57 C++ Operatoren überladen
-
Der Aufruf ist möglich
1.2 Die C++ Operatoren
Dieses Kapitel ist eine Übersicht über alle C++ Operatoren. Die Tabelle ist dabei nach der Priorität der Operatoren geordnet. Außerdem enthält sie noch die Informationen über die Auswertungs-Reihenfolge (L=>R oder R=>L), und ob sich der Operator überladen läßt (+) oder nicht (-) - und wenn, ob nur als Element-Funktion (EF), oder als freie Funktion und Klassen-Funktion (+, (KF)).Prio. | Operator | Beschreibung | Ausw. | Überl. |
---|---|---|---|---|
1 | :: | Bereichszuordnung | L=>R | - |
. | Komponenten-Zugriff | L=>R | - | |
2 | .-> | Element-Zugriff, bzw. Punkt-Pfeil-Operator | L=>R | + |
-> | Element-Zugriff, bzw. Pfeil-Operator | L=>R | EF | |
[ ] | Index (Array-Zugriff) | L=>R | EF | |
( ) | Funktions-Aufruf | L=>R | EF | |
++ | Post-Increment (n++) | L=>R | + | |
-- | Post-Decrement (n--) | L=>R | + | |
typeid | Typ (RTTI) | L=>R | - | |
const_cast | Const-Cast | L=>R | - | |
static_cast | Static-Cast | L=>R | - | |
reinterpret_cast | Reinterpret-Cast | L=>R | - | |
dynamic_cast | Dynamic-Cast | L=>R | - | |
3 | sizeof | Primäre Objekt-Größe | R=>L | - |
++ | Prä-Increment (++n) | R=>L | + | |
-- | Prä-Decrement (--n) | R=>L | + | |
~ | 1-er Komplement | R=>L | + | |
! | Logisches Not | R=>L | + | |
+ | Unäres + (Vorzeichen) | R=>L | + | |
- | Unäres - (Vorzeichen) | R=>L | + | |
& | Unäres & (Adresse) | R=>L | + | |
* | Unäres * (Dereferenzierung) | R=>L | + | |
new, delete, new[], ... | Dynamische Speicherverwaltung | R=>L | + (KF) | |
( ) | Klassischer C-Cast | R=>L | - | |
4 | .* | Zeiger auf Element | L=>R | - |
->* | Zeiger auf Element | L=>R | + | |
5 | * | Multiplikation (binäres *) | L=>R | + |
/ | Division | L=>R | + | |
% | Modulo | L=>R | + | |
6 | + | Addition (binäres +) | L=>R | + |
- | Subtraktion (binäres -) | L=>R | + | |
7 | << | Ausgabe bzw. Bit-Links-Schiebe | L=>R | + |
>> | Eingabe bzw. Bit-Rechts-Schiebe | L=>R | + | |
8 | < | Kleiner | L=>R | + |
> | Größer | L=>R | + | |
<= | Kleiner-Gleich | L=>R | + | |
>= | Größer-Gleich | L=>R | + | |
9 | == | Gleich | L=>R | + |
!= | Ungleich | L=>R | + | |
10 | & | Bitweises Und (binäres &) | L=>R | + |
11 | ^ | Bitweises XOR | L=>R | + |
12 | | | Bitweises Oder | L=>R | + |
13 | && | Logisches Und | L=>R | + |
14 | || | Logisches Oder | L=>R | + |
15 | ? : | Bedingung | R=>L | - |
16 | = | Zuweisung | R=>L | EF |
*= | Multiplikations-Zuweisung | R=>L | EF | |
/= | Divisions-Zuweisung | R=>L | EF | |
%= | Modulo-Zuweisung | R=>L | EF | |
+= | Additions-Zuweisung | R=>L | EF | |
-= | Subtraktions-Zuweisung | R=>L | EF | |
<<= | Links-Bit-Schiebe-Zuweisung | R=>L | EF | |
>>= | Rechts-Bit-Schiebe-Zuweisung | R=>L | EF | |
&= | Bitweise-Und-Zuweisung | R=>L | EF | |
|= | Bitweise-Oder-Zuweisung | R=>L | EF | |
^= | Bitweise-Xor-Zuweisung | R=>L | EF | |
17 | , | Komma | L=>R | + |
-
Zusätzlich lassen sich noch:
-
Konvertierungs-Operatoren als Element-Funktinonen definieren
- Diese stehen dann für implizite benutzerdefinierte Typ-Umwandlungen - z.B. bei Funktions-Aufrufen - zur Verfügung
- Seit C++11 können diese auch "explizit" gemacht werden, und für explizite-bool Konvertierungen sind Bool-Kontexte definiert worden
- Siehe Kapitel 3.11
-
Benutzer-definierte Literale definieren
- Hiermit können eigene typisierte Literale erzeugt werden
- Dieses Feature ist neu mit C++11 eingeführt worden
- Siehe Kapitel 3.12
-
Konvertierungs-Operatoren als Element-Funktinonen definieren
2. Grundlagen
-
Einführungs-Beispiel
-
Klasse für Brüche: "Rational"
- Hinweis - in der C++ Standard-Bibliothek gibt es eine solche Klasse
- Diese Beispiel-Klasse ist viel viel einfacher, und auch nicht wirklich durchdacht
- Es geht hier ja um Operator-Überladung, und nicht um die Implementierung einer Bruch-Klasse
-
2 Attribute für Zähler & Nenner
- Zähler (Numerator) mit Name "nume" und Typ "int"
- Nenner (Denominator) mit Name "deno" und Typ "int"
- Die Namen sind so unschön (aber kurz), damit die Code-Beispiele nicht zu breit werden
-
Wir ignorieren Probleme wie:
- "int" ist vielleicht nicht der beste Typ für die Attribute
- Umsetzung vielleicht besser als Template-Klasse
- Kürzen des Bruchs - vielleicht sogar automatisch
- usw.
-
Und wir implementieren auch nur eine ganz einfache Operation
- Die Multiplikation "*"
- Implementierung: Zähler*Zähler und Nenner*Nenner
- Es geht nicht um die Bruch-Klasse, sondern um Operator-Überladung
-
Klasse für Brüche: "Rational"
2.1 Operatoren als Element-Funktionen
-
Zuerst einmal ganz ohne Operator-Überladung
- In Realität würde man ja auch die Ausgabe mit Operator-Überladung statt mit einer Print-Funktion umsetzen. Aber wir können ja noch keine Operator-Überladung
-
Multiplikation mit einer Element-Funktion "mul"
- Ganz normal runter-programmiert
- So ungefähr würde Ihre Klasse wohl auch aussehen - hoffe ich
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
Rational mul(const Rational&) const;
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational Rational::mul(const Rational& rhs) const
{
return Rational(nume*rhs.nume, deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = r1.mul(r2);
r0.print();
}
Ausgabe:
0/1
2/3
5/7
10/21
-
Und jetzt das Gleiche zusätzlich mit Operator-Überladung
- Natürlich analog als Element-Funktion
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
Rational mul(const Rational&) const;
Rational operator*(const Rational&) const;
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational Rational::mul(const Rational& rhs) const
{
return Rational(nume*rhs.nume, deno*rhs.deno);
}
Rational Rational::operator*(const Rational& rhs) const
{
return Rational(nume*rhs.nume, deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = r1.mul(r2);
r0.print();
r0 = r1.operator*(r2); // Funktions-Schreibweise
r0.print();
r0 = r1*r2; // Operator-Schreibweise
r0.print();
}
Ausgabe:
0/1
2/3
5/7
10/21
10/21
10/21
- Operatoren sind quasi wie normale Funktionen
-
Unterschiede:
-
Der Name
- Ist vorgegeben
- Schlüsselwort "operator" mit folgendem Operator
-
Der Aufruf
- Es ist die bekannte Funktions-Schreibweise beim Aufruf zugelassen
- Und es ist die Operator-Schreibweise beim Aufruf zugelassen
- In Kapitel 2.4 vergleichen wir beide Aufruf-Schreibweisen noch kurz
-
Der Name
-
Der Rest ist identisch
- Also keine Panik
- Operator-Überladung ist ganz einfach
2.1.1 Vergessen Sie nicht das implizite "this"
-
Noch mal ein Hinweis speziell für Einsteiger in C++
- Operatoren mit 2 Parametern haben als Element-Funktion nur einen expliziten Parameter!
-
Vergessen Sie bitte nicht den impliziten This-Parameter
- Das Objekt, für das Sie die Element-Funktion aufrufen
- Element-Funktionen haben ja immer einen Objekt-Bezug, d.h. lassen sich nur für ein Objekt aufrufen
- Schauen Sie sich bitte das folgende Beispiel noch einmal unter diesem Gesichtpunkt an
-
Dieser Hinweis ist nur für Einsteiger in C++ gedacht:
- Fortgeschrittene OO oder C++ Programmierer sollten dieses Problem nicht haben
- Aber bei C++ Einsteigern erlebe ich das häufig, dass Sie sich anfänglich mit Klassen und Element-Funktionen schwer tun. Und spätestens bei den Operatoren wird dann das implizite This-Objekt gerne wieder vergessen.
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
// Richtig, dies ist der Operator "*" mit 2 Parametern (binärer Operator):
// - ein impliziter Parameter ("this"), da Element-Funktion
// - und der exlizite Parameter "rhs"
Rational operator*(const Rational& rhs) const;
// Compiler-Fehler - dies wäre ein Operator "*" mit 3 Parametern:
// - ein impliziter Parameter ("this"), da Element-Funktion
// - und zwei exlizite Parameter "lhs" und "rhs"
// => Compiler-Fehler, da der Operator "*" keine 3 Parameter hat
Rational operator*(const Rational& lhs, const Rational& rhs) const;
private:
int nume, deno;
};
// Dies ist die Implementierung des Operator "*" als Element-Funktion
// Der Operator "*" hat hier 2 Parameter (binärer Operator):
// - ein impliziter Parameter ("this"), da der Operator eine Element-Funktion ist
// Daher kann auch direkt auf die Attribute "nume" und "deno" zugegriffen werden
// - und der exlizite Parameter "rhs"
Rational Rational::operator*(const Rational& rhs) const
{
return Rational(nume*rhs.nume, deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7), r3;
// Als Element-Funktion hat der Operator "*" nur einen expliziten Parameter
r0 = r1.operator*(r2);
r0 = r1 * r2;
// Compiler-Fehler - Aufruf mit 3 Parametern
// Was soll das auch? Wozu sollte "r3" da sein?
r0 = r1.operator*(r2, r3);
}
-
Also:
- Als Element-Funktion hat ein Operator einen expliziten Parameter weniger als er Operanden hat
- Vergleiche auch Kapitel 3.1 über unäre Operatoren
2.2 Operatoren als freie Funktionen
-
Im Prinzip das gleiche Beispiel wie eben
- Wieder die Bruch-Klasse
-
Nur diesmal die Multiplikation mit einer globalen Funktion "mul" umgesetzt
- Hinweis: die globale Funktion ist als "friend" der Klasse "Rational" deklariert
- Damit sparen wir uns die Getter-Funktionen für "Zähler" und "Nenner"
- Ansonsten identisch
- Und natürlich wieder erstmal ohne Operator-Überladung
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
friend Rational mul(const Rational&, const Rational&);
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational mul(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.nume*rhs.nume, lhs.deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = mul(r1, r2);
r0.print();
}
Ausgabe:
0/1
2/3
5/7
10/21
-
Und jetzt das Gleiche zusätzlich mit Operator-Überladung
- Natürlich analog als freie Funktion
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
friend Rational mul(const Rational&, const Rational&);
friend Rational operator*(const Rational&, const Rational&);
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational mul(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.nume*rhs.nume, lhs.deno*rhs.deno);
}
Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.nume*rhs.nume, lhs.deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = mul(r1, r2);
r0.print();
r0 = operator*(r1, r2); // Funktions-Schreibweise
r0.print();
r0 = r1*r2; // Operator-Schreibweise
r0.print();
}
Ausgabe:
0/1
2/3
5/7
10/21
10/21
10/21
- Operatoren sind quasi wie normale Funktionen
-
Unterschiede:
-
Der Name
- Ist vorgegeben
- Schlüsselwort "operator" mit folgendem Operator
-
Der Aufruf
- Es ist die bekannte Funktions-Schreibweise beim Aufruf zugelassen
- Und es ist die Operator-Schreibweise beim Aufruf zugelassen
- In Kapitel 2.4 vergleichen wir beide Aufruf-Schreibweisen noch kurz
-
Der Name
-
Der Rest ist identisch
- Also keine Panik
- Operator-Überladung ist ganz einfach
2.3 Der Vergleich zwischen beiden Varianten
-
Wir haben gesehen - wir können Operator-Funktionen überladen als:
- Element-Funktion (immer, außer bei "new" & "delete")
- Freie-Funktion (fast immer)
-
Und was ist jetzt besser? Welche Variante nimmt man?
- Beide Lösungen sind prinzipiell identisch.
-
Vorzuziehen sind Element-Funktionen:
- Sind semantisch der Klasse zugeordnet
- Haben Zugriff auf alle Elemente der Klasse
- Können virtual sein und überschrieben werden
- Bei manchen Operatoren geht es nur so
-
Aber manchmal geht es nur mit freien Funktionen:
- Freie Operatoren können symmetrisch arbeiten
- Freie Operatoren können für fremde Klassen definiert werden
- Freie Operatoren können für Enum-Typen definiert werden
- => In diesen Fällen überladen wir den Operator als freie Funktion
2.3.1 Symmetrische Operator-Nutzung
- Für beide Operanden soll die implizite Typ-Umwandlung funktionieren
- Geht für den linken Operanden nicht bei Element-Funktionen
- Typisches Problem in mathematischen Domainen
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
Rational operator*(const Rational&) const;
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational Rational::operator*(const Rational& rhs) const
{
return Rational(nume*rhs.nume, deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = r1 * 4; // Okay => r0 = r1 * Rational(4);
r0.print();
r0 = 4 * r2; // Compiler-Fehler - geht nicht bei Element-Funktionen
r0.print();
}
- Aber bei freien Funktionen funktioniert das:
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
friend Rational operator*(const Rational&, const Rational&);
void print() const { cout << nume << '/' << deno << endl; }
private:
int nume, deno;
};
Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.nume*rhs.nume, lhs.deno*rhs.deno);
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
r0.print();
r1.print();
r2.print();
r0 = r1 * 4; // Okay => r0 = r1 * Rational(4);
r0.print();
r0 = 4 * r2; // Okay => r0 = Rational(4) * r2;
r0.print();
}
Ausgabe:
0/1
2/3
5/7
8/3
20/7
2.3.2 Operatoren für fremde Klassen
-
In fremde Klassen können keine Element-Funktionen von außen injiziert werden
-
Z.B. Ausgabe-Operator "<<" für die Rational-Klasse
- Dazu müßte man eigentlich "std::ostream" erweitern
- Bzw. genau genommen die zugrunde liegende Template-Klasse "std::basic_ostream"
-
Dies geht nur durch einen Antrag beim ISO C++ Standardisierungs-Gremium ;-)
- Dauert lange (frühestens C++1y, der wahrscheinlich 2014 kommt)
- Verspricht wenig Aussicht auf Erfolg
-
Z.B. Ausgabe-Operator "<<" für die Rational-Klasse
// So geht es leider nicht
namespace std
{
template<...> class basic_ostream
{
public:
...
ostream& operator<<(const Rational&); // Sehr sehr unwahrscheinlich (siehe Text)
...
};
}
- Statt dessen muss man eine freie Operator-Funktion definieren:
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int n=0, int d=1) : nume(n), deno(d) {}
friend Rational operator*(const Rational&, const Rational&);
friend ostream& operator<<(ostream&, const Rational&);
private:
int nume, deno;
};
Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.nume*rhs.nume, lhs.deno*rhs.deno);
}
ostream& operator<<(ostream& out, const Rational& arg)
{
return out << arg.nume << '/' << arg.deno;
}
int main()
{
Rational r0, r1(2, 3), r2(5, 7);
cout << r0 << endl;
cout << r1 << endl;
cout << r2 << endl;
r0 = r1 * r2;
cout << r0 << endl;
r0 = r1 * 4;
cout << r0 << endl;
r0 = 4 * r2;
cout << r0 << endl;
}
Ausgabe:
0/1
2/3
5/7
10/21
8/3
20/7
- Weitere Beispiele für freie Operator-Funktionen aufgrund fremder Klassen finden Sie in Kapitel 2.3.5
2.3.3 Operatoren für Enums
-
Enums haben keine Element-Funktionen
- Aber Enums sind benutzerdefinierte Typen
- Daher lassen sich für Enums Operatoren überladen
- Aber es müssen freie Operator-Funktionen sein, da Enums ja keine Element-Funktionen haben
#include <iostream>
using namespace std;
enum E { one, two, three };
inline ostream& operator<<(ostream& out, E e)
{
static const char* text[] = { "eins", "zwei", "drei" };
return out << text[e];
}
inline E& operator++(E& e)
{
static E next[] = { two, three, one };
return e=next[e];
}
int main()
{
E e = one;
cout << e << endl; // => eins
++e;
cout << e << endl; // => zwei
++e;
cout << e << endl; // => drei
++e;
cout << e << endl; // => eins
++++e;
cout << e << endl; // => drei
}
Ausgabe:
eins
zwei
drei
eins
drei
2.3.4 Zusammenfassung
- Bevorzugen Sie Element-Operator-Funktionen
-
Aber bei 3 Problemen nutzen Sie freie Operator-Funktionen:
- Symmetrische Nutzung
- Operatoren für fremde Klassen
- Operatoren für Enums
2.3.5 Zwei weitere Beispiele für freie Operator-Funktionen
- Noch zwei Beispiele mit Strings
-
Strings sind auch "fremde" Klassen
- Daher können wir sie nicht direkt erweitern
- Sondern müssen statt dessen eine freie Operator-Funktion wählen
- Beispiel 1:
- Addition von "std::string" + "int"
- Hinweis - ich benutzt hier intern für die Wandlung von "int" zu "std::string" Boost.LexicalCast[1][2].
#include <iostream>
#include <string>
#include <boost/lexical_cast.hpp>
using namespace std;
string operator+(string s, int n)
{
s += boost::lexical_cast<string>(n);
return s;
}
int main()
{
string s("abc->");
cout << s << endl; // abc->
s = s + 2;
cout << s << endl; // abc->2
s = s + 34;
cout << s << endl; // abc->234
}
Ausgabe:
abc->
abc->2
abc->234
- Beispiel 2:
- An String wie an Streams mit dem Operator << Werte anhängen
- Hinweis - ich benutzt hier intern für die Wandlung von "int" zu "std::string" Boost.LexicalCast[1][2].
#include <iostream>
#include <string>
#include <boost/lexical_cast.hpp>
using namespace std;
string& operator<<(string& s, bool b)
{
s += b ? "true" : "false";
return s;
}
template<class T> string& operator<<(string& s, const T& t)
{
s += boost::lexical_cast<string>(t);
return s;
}
int main()
{
string s;
s << true << " ist: " << 1 << " + " << 2 << ' ' << "= " << 3;
cout << s << endl;
(s="") << 3.5 << ' ' << false;
cout << s << endl;
}
Ausgabe:
true ist: 1 + 2 = 3
3.5 false
2.4 Sinn der funktionalen Schreibweise beim Aufruf
-
Wozu benötigt man die funktionale Schreibweise beim Aufruf?
- Im Normalfall wird man natürlich immer die Operator-Schreibweise wählen
- Dafür hat man ja schließlich die Operatoren definiert
-
Aber in manchen Spezial-Situationen benötigt man die funktionale Schreibweise beim Aufruf
- Denn nur Operator-Aufrufe in funktionaler Schreibweise kann man vollständig qualifizieren[3] - siehe Beispiel
- Bei funktionaler Schreibweise kann man den Dummy-Parameter beim Post-Inkrement und -Dekrement Operator angegeben - siehe Kapitel 3.4
- Der Pfeil-Operator "->" läßt sich nur in funktionaler Schreibweise bei einem Non-Pointer- oder Non-Proxy Rückgabe-Typ aufrufen - siehe Kapitel 3.8
-
Beispiel für die vollständige Qualifizierung des Operators:
- Aufruf des überschriebenen Operators aus der Basisklasse
- Hinweis - ich benutze im folgenden Beispiel das C++11 Feature "override". Falls Sie es noch nicht kennen - ich stelle es in diesem Artikel[4] vor. Falls Ihr Compiler "override" noch nicht unterstützt - dann lassen Sie das "override" im Quelltext einfach weg.
#include <iostream>
#include <string>
using namespace std;
class A
{
public:
virtual string operator+(const A&) const;
};
string A::operator+(const A&) const
{
return "A";
}
class B : public A
{
public:
virtual string operator+(const A&) const override;
};
string B::operator+(const A& a) const
{
string res = A::operator+(a); // Aufruf der Basis-Klassen-Funktion geht nur so
res += "B";
return res;
}
int main()
{
A a1, a2;
string res = a1 + a2;
cout << res << endl;
B b;
res = b + a2;
cout << res << endl;
}
Ausgabe:
A
AB
2.5 Adressen von überladenen Operatoren
- Überladene Operatoren sind normale Funktionen
- Daher kann man von ihnen auch die Adresse erfragen und nutzen
- Achtung - dies muss mit dem Schlüsselwort "operator" geschehen - siehe Beispiel
- Hinweis - und vergessen Sie bei Element-Funktionen nicht das "&" vor dem Operator
struct A
{
};
A operator+(const A&, const A&);
struct B
{
B operator+(const B&) const;
};
int main()
{
A(*plus1)(const A&, const A&) = +; // Compiler-Fehler, so geht das nicht
A(*plus2)(const A&, const A&) = operator+; // Mit "operator" funktioniert es
B(B::*plus3)(const B&) const = &B::operator+; // Und so fuer Element-Funktionen
}
2.6 Noch eine Bemerkung zu den Parameter-Namen
-
Bei binären Operatoren finden Sie häufig die Parameter-Namen "lhs" und "rhs"
- Auch ich verwende sie in den Beispielen
-
Die Namen stehen für:
-
"lhs" : "left hand side"
- Der Operand, der auf der linken Seite des Operators steht
-
"rhs" : "right hand side"
- Der Operand, der auf der rechten Seite des Operators steht
-
"lhs" : "left hand side"
-
Es sind ganz tpyische Namen für die Parameter von Operatoren
- Nutzen Sie sie ruhig auch, da hier jeder automatisch die richtige Assoziation hat
3. Operator-Besonderheiten
3.1 Unäre Operatoren
-
In C++ gibt es unäre Operatoren, daher Operatoren mit nur einem Parameter - z.B.:
- Die Vorzeichen "+" und "-"
- Der Adress-Operator "&"
- Der Dereferenzierungs-Operator "*"
- Das logische Not "!"
- Das 1-er Komplement "~"
- Im Prinzip gibt es zu den unären Operatoren an sich nichts zu sagen...
-
Aber, in der Praxis tun sich Einsteiger immer mal wieder schwer mit ihnen
- Denn als Element-Funktion implementiert haben sie keinen expliziten Parameter
- Sie haben nur den impliziten "this" Parameter
- Und das sieht - zumindest für den Einsteiger - komisch aus
- Schauen Sie sich das folgende Code-Beispiel an
#include <iostream>
using namespace std;
struct A
{
A(int v=0) : value(v) {}
// Element-Funktion => kein expliziter Parameter
// Nur der implizite "this" Parameter
bool operator!() const;
// Als freie Funktion => natuerlich den einen expliziten Parameter
friend A operator-(const A&);
friend inline ostream& operator<<(ostream& out, const A& a)
{
return out << "A(" << a.value << ')';
}
private:
int value;
};
// Element-Funktion => kein expliziter Parameter
bool A::operator!() const
{
return value==0;
}
// Als freie Funktion => natuerlich den einen expliziten Parameter
A operator-(const A& a)
{
return A(-a.value);
}
int main()
{
A a0;
if (!a0)
{
cout << "Not a0 " << a0 << " ist true" << endl;
}
else
{
cout << "Not a0 " << a0 << " ist false" << endl;
}
A a1(1);
if (!a1)
{
cout << "Not a1 " << a1 << " ist true" << endl;
}
else
{
cout << "Not a1 " << a1 << " ist false" << endl;
}
a0 = -a1;
cout << "a0 == " << a0 << endl;
cout << "a1 == " << a1 << endl;
}
Ausgabe:
Not a0 A(0) ist true
Not a1 A(1) ist false
a0 == A(-1)
a1 == A(1)
-
Achtung - viele der unären Operatoren gibt es auch in einer binären Variante - z.B.:
- Das Vorzeichen "+" (unär) bzw. die Addition "+" (binär)
- Die Dereferenzierung "*" (unär) bzw. die Multiplikation "*" (binär)
- Beide Varianten können problemlos nebeneinander existieren und parallel überladen werden
- Der Compiler erkennt an der Anzahl der Parameter, welcher Operator gemeint ist
- Hinweis - das folgende Beispiel ist inhaltlich etwas sinnlos, zeigt aber die mögliche parallele "Existenz" des gleichen unären und binären Operators
#include <iostream>
using namespace std;
// Klasse A - unaeres und binaeres + als Element-Funktion
struct A
{
A& operator+();
void operator+(const A&) const;
};
A& A::operator+()
{
cout << "- Klasse A - unaeres +" << endl;
return *this;
}
void A::operator+(const A&) const
{
cout << "- Klasse A - binaeres +" << endl;
}
// Klasse B - unaeres und binaeres + als freie Funktion
struct B
{
};
B& operator+(B&);
void operator+(const B&, const B&);
B& operator+(B& b)
{
cout << "- Klasse B - unaeres +" << endl;
return b;
}
void operator+(const B&, const B&)
{
cout << "- Klasse B - binaeres +" << endl;
}
int main()
{
A a1, a2;
cout << "+a1 =>" << endl;
+a1;
cout << "a1+a2 =>" << endl;
a1+a2;
cout << "a1 + +a2 =>" << endl;
a1 + +a2;
cout << endl;
B b1, b2;
cout << "+b1 =>" << endl;
+b1;
cout << "b1+b2 =>" << endl;
b1+b2;
cout << "b1 + +b2 =>" << endl;
b1 + +b2;
}
Ausgabe:
+a1 =>
- Klasse A - unaeres +
a1+a2 =>
- Klasse A - binaeres +
a1 + +a2 =>
- Klasse A - unaeres +
- Klasse A - binaeres +
+b1 =>
- Klasse B - unaeres +
b1+b2 =>
- Klasse B - binaeres +
b1 + +b2 =>
- Klasse B - unaeres +
- Klasse B - binaeres +
-
Noch ein Hinweis:
-
Bei binären Operatoren bindet das "this" an den linken Operanden, d.h. an den Operanden der links vom
Operator steht. Und der rechte Operand wird an die überladene Operator-Funktion als Argument übergeben.
- Vergleiche auch Kapitel 2.1.1 über das implizite "this".
-
Bei unären Operatoren steht beim Aufruf das angesprochene Objekt aber rechts vom Operator,
müßte also eigentlich als Parameter übergeben werden.
- Manche C++ Einsteiger empfinden dies als uneinheitlich bzw. inkonsistent.
- Das kann man so sehen, aber was soll man machen, wenn man nur einen Operanden hat?
-
Bei binären Operatoren bindet das "this" an den linken Operanden, d.h. an den Operanden der links vom
Operator steht. Und der rechte Operand wird an die überladene Operator-Funktion als Argument übergeben.
-
Vielleicht sollte die Regel so heißen:
-
Bei einer Operator-Element-Funktion wird der linkste Operand an "this" gebunden.
Wenn der rechte Operand auch der Linkste ist, dann eben an den.
-
Bei einer Operator-Element-Funktion wird der linkste Operand an "this" gebunden.
Liste aller überladbaren unären Operatoren in C++:
Operator | Siehe auch: |
---|---|
1-er Komplement; ~ | --- |
Logisches Not: ! | --- |
Unäres + (Vorzeichen) | --- |
Unäres - (Vorzeichen) | --- |
Unäres * (Dereferenzierung) | --- |
Post- bzw. Prä-Increment: n++ bzw. ++n | Kapitel 3.4 |
Post- bzw. Prä-Decrement: n-- bzw. --n | Kapitel 3.4 |
Unäres & (Adresse) | Kapitel 3.6 |
Element-Zugriff: -> | Kapitel 3.8 |
Funktions-Aufruf: () | Kapitel 3.10 |
3.2 Zuweisungs-Operator ("=")
-
Der Zuweisungs-Operator "=" kann überladen werden
- Werden für eine Klasse mehrere Zuweisungs-Operatoren implementiert - die sich durch die Parameterliste unterscheiden müssen - dann kann eine Klasse mehrere Zuweisungs-Operatoren enthalten.
-
Hierbei wird zwischen den Kopier-Zuweisungs-Operatoren (max. 2 Stück) und
allen restlichen Zuweisungs-Operatoren (beliebig viele) unterschieden.
- Zusätzlich können noch alle Operations-Zuweisungen wie z.B. "+=" oder "*=" überladen werden.
- Hinweis - alle Zuweisungs-Operatoren können nur als Element-Funktionen überladen werden, nicht als freie Funktionen!
3.2.1 Kopier-Zuweisungs-Operator
-
Kopier-Zuweisungs-Operator
-
Ist die Zuweisung, die ein Objekt der Klasse selber erwartet
-
Typischerweise als Const-Referenz
- Denn man will das Quell-Objekt im Normallfall nicht verändern - und Kopien sind zu langsam
- Aber es funktioniert auch als Non-Const-Referenz (auch parallel zu der Const-Referenz)
- Und auch mit Kopie ist es der Kopier-Zuweisungs-Operator, aber diese Variante ist für Klassen eher uninteressant
-
Typischerweise als Const-Referenz
- Und der Kopier-Zuweisungs-Operator ist - wie alle Zuweisungen - typischerweise eine Non-Const-Element-Funktion. Denn man will das Ziel-Objekt ja verändern (das Quell-Objekt zuweisen) - da macht eine Const-Element-Funktion eher keinen Sinn.
-
Typische Rückgabe:
-
Objekt selber, damit man es direkt wieder im Ausdruck wiederverwenden kann,
z.B. bei Mehrfach-Zuweisungen - Also als Referenz
- Nicht als "const" (C++), obwohl in C die Rückgabe ein R-Value ist
-
Objekt selber, damit man es direkt wieder im Ausdruck wiederverwenden kann,
- Kann - wie alle Zuweisungs-Operatoren - nur als Element-Funktion überladen werden
-
Ist die Zuweisung, die ein Objekt der Klasse selber erwartet
#include <iostream>
using namespace std;
class A
{
public:
explicit A(int arg=0) : n(arg) {}
A& operator=(const A&);
friend ostream& operator<<(ostream& out, const A& a)
{
return out <<"A(" << a.n << ')';
}
private:
int n;
};
A& A::operator=(const A& a)
{
n = a.n;
return *this;
}
int main()
{
A a0, a1(1), a2(2);
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
a0 = a1;
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
a0 = a1 = a2; // Mehrfach-Zuweisung
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
}
Ausgabe:
A(0) A(1) A(2)
A(1) A(1) A(2)
A(2) A(2) A(2)
-
Der Kopier-Zuweisungs-Operator wird immer automatisch vom Compiler erstellt:
- Wenn man selber keinen Kopier-Zuweisungs-Operator deklariert
- Zuweisung ist also erstmal immer möglich
-
Automatischer Kopier-Zuweisungs-Operator
- Auch "Impliziter Kopier-Zuweisungs-Operator" genannt
- Ruft für jedes Element der Klasse (inkl. Basisklassen) selber wieder den Kopier-Zuweisungs-Operator auf
- Gehört zur Regel-der-3 (siehe gleich)
// Im Prinzip der gleiche Code wie eben
// Aber nun mit "implizitem Kopier-Zuweisungs-Operator"
#include <iostream>
using namespace std;
class A
{
public:
explicit A(int arg=0) : n(arg) {}
// Kein expliziter Kopier-Zuweisungs-Operator mehr => impliziter
friend ostream& operator<<(ostream& out, const A& a)
{
return out <<"A(" << a.n << ')';
}
private:
int n;
};
int main()
{
A a0, a1(1), a2(2);
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
a0 = a1; // Funktioniert auch so
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
a0 = a1 = a2; // Funktioniert auch
cout << a0 << ' ' << a1 << ' ' << a2 << endl;
}
Ausgabe:
A(0) A(1) A(2)
A(1) A(1) A(2)
A(2) A(2) A(2)
-
Regel-der-3:
-
Wenn man einen der folgenden drei Element-Funktionen selber implementieren muss,
dann muss man sehr wahrscheinlich alle drei selber implementieren.
- Destruktor
- Kopier-Konstruktor
- Kopier-Zuweisungs-Operator
-
Wenn man einen der folgenden drei Element-Funktionen selber implementieren muss,
dann muss man sehr wahrscheinlich alle drei selber implementieren.
- Beispiele, Details und Hintergründe zu dieser Regel sollten sich in jedem guten C++ Lehrbuch (z.B. hier[5]) oder in anderer weiterführender Literatur[6][7] finden.
- Diese Regel gehört neben RAII[8][9] und den Parameter-Übergabe-Konventionen[10][11] zu den 3 wichtigsten Regeln von C++ - wahrscheinlich ist sie sogar die Wichtigste.
-
Der folgende Code enthält ein angedeutes Beispiel für eine eigene "fehlerhafte" String-Klasse
- Sie enthält die Element-Funktionen der "Regel der 3" nur implizit
- Sie ist daher voller grundlegender Fehler - also so niemals implementieren!
// Achtung - stark fehlerhafter Code
// So niemals machen
#include <cstring>
#include <iostream>
using namespace std;
class MyString
{
public:
MyString(const char*);
// Achtung - die impliziten Elemente sind alle fehlerhaft - so nicht machen
// Kein expliziter Destruktor => impliziter
// Kein expliziter Kopier-Konstruktor => impliziter
// Kein expliziter Kopier-Zuweisungs-Operator => impliziter
friend ostream& operator<<(ostream&, const MyString&);
private:
char* p;
};
MyString::MyString(const char* q)
{
p = new char[strlen(q)+1];
strcpy(p, q);
}
ostream& operator<<(ostream& out, const MyString& str)
{
out << str.p;
return out;
}
int main()
{
// Beispiel-Nutzung mit einigen Problemen...
MyString s1("Hallo");
cout << "s1: " << s1 << endl;
// Fehlender korrekter Kopier-Konstruktor sorgt fuer Laufzeit-Probleme
MyString s2(s1);
cout << "s2: " << s2 << endl;
// Fehlender korrekter Kopier-Zuweisungs-Operator sorgt fuer Laufzeit-Probleme
s1 = s2;
cout << "s1: " << s1 << endl;
// Fehlender korrekter Destruktor sorgt fuer Speicher-Loecher
}
Ausgabe:
s1: Hallo
s2: Hallo
s1: Hallo
-
Man kann den Kopier-Zuweisungs-Operator verbieten
- Wenn der implizite nicht korrekt funktioniert
- Und man ihn selber nicht implementieren will oder kann
-
3 Möglichkeiten, ihn zu verbieten
-
Deklarieren, aber nicht implementieren
- Da der Kopier-Zuweisungs-Operator deklariert ist, erzeugt der Compiler keinen Impliziten
- Am Besten "private" deklarieren und mit einem entsprechenden Kommentar versehen
- Führt meist zu einem Compiler-Fehler (kein Zugriff wegen "private")
- Oder auf jeden Fall zu einem Linker-Fehler (Funktion nicht definiert)
- Dies ist viele Jahre ein typisches C++ Idiom von C++98 und C++03 gewesen
-
Klasse "private" ableiten von "boost::noncopyable"
- Achtung – verbietet auch das Kopieren
- Aber das will man dann ja auch meistens
- Die Alternative aus Boost in C++98 und C++03 zum typischen C++ Idiom oben
-
In C++11 auf "= delete" setzen
- Aber das ist C++11
- Und dieses C++11 Feature wird aktuell (16.05.2012) von kaum einem (keinem?) Compiler unterstützt
-
Deklarieren, aber nicht implementieren
// Variante 1 - Kopier-Zuweisungs-Operator deklarieren, aber nicht implementieren
class A
{
private:
// Deklaration private und ohne Implementierung, da die Klasse nicht zuweisbar sein soll
A& operator=(const A&);
};
// Kopier-Zuweisungs-Operator von A nicht implementieren
int main()
{
A a1, a2;
a1 = a2; // Compiler-Fehler, da kein Zugriff (Element private)
} // Bei moeglichem Zugriff (friend, ...) => Linker-Fehler
// Variante 2 - Nutzung von Boost::noncopyable
#include <boost/noncopyable.hpp>
class A : boost::noncopyable
{
};
int main()
{
A a1, a2;
a1 = a2; // Compiler-Fehler
}
// Variante 3 - C++11
class A
{
public:
A& operator=(const A&) = delete;
};
int main()
{
A a1, a2;
a1 = a2; // Compiler-Fehler
}
-
Zuweisungen können natürlich auch überladen werden
- Also weitere Zuweisungen, parallel oder statt des Kopier-Zuweisungs-Operators
- Es kann also mehrere Zuweisungen parallel geben
-
Im folgenden Beispiel neben dem expliziten Kopier-Zuweisungs-Operator noch:
- Einen Bool-Zuweisungs-Operator
- Einen Int-Zuweisungs-Operator
- Hinweis: im folgenden Beispiel sind die Klasse "A" und die Implementierungen der Bool- und Int-Zuweisungs-Operatoren ziemlich sinnfrei. Primär geht es hier ja nur darum, zu zeigen, dass viele Zuweisungs-Operatoren parallel vorkommen können. Und sekundär sollten die Zuweisungs-Operatoren Code enthalten, die an der Ausgabe den Operator eindeutig identifizieren.
#include <iostream>
using namespace std;
class A
{
public:
A(int v1, int v2) : value1(v1), value2(v2) {}
A& operator=(const A&); // Expliziter Kopier-Zuweisungs-Operator
A& operator=(bool); // Weiterer anderer Zuweisungs-Operator
A& operator=(int); // Weiterer anderer Zuweisungs-Operator
friend ostream& operator<<(ostream& out, const A& a)
{
return out << '[' << a.value1 << ',' << a.value2 << ']';
}
private:
int value1, value2;
};
// Expliziter Kopier-Zuweisungs-Operator
A& A::operator=(const A& a)
{
value1 = a.value1;
value2 = a.value2;
return *this;
}
A& A::operator=(bool b)
{
value1 = b;
value2 = !b;
return *this;
}
A& A::operator=(int v)
{
value1 = v;
value2 = -v;
return *this;
}
int main()
{
A a1(1, 11), a2(2, 22);
cout << "a1=" << a1 << endl;
cout << "a2=" << a2 << endl;
a1 = a2;
cout << "a1=" << a1 << endl;
a1 = true;
cout << "a1=" << a1 << endl;
a1 = 42;
cout << "a1=" << a1 << endl;
}
Ausgabe:
a1=[1,11]
a2=[2,22]
a1=[2,22]
a1=[1,0]
a1=[42,-42]
-
Die Erzeugung des Kopier-Zuweisungs-Operators ist unabhängig von den weiteren überladenen Zuweisungs-Operatoren
- Im Beispiel gibt es keinen expliziten Kopier-Zuweisungs-Operator
- Aber zwei weitere andere Zuweisungs-Operatoren
-
Dann wird der implizite (automatische) Kopier-Zuweisungs-Operator erzeugt
- Denn es ist kein Expliziter vorhanden
- Die weiteren Zuweisungs-Operatoren spielen hierfür keine Rolle
-
Im Prinzip ist es wie beim Kopier-Konstruktor
- Dessen implizite Erzeugung ist auch nur von expliziten Kopier-Konstruktoren abhängig
- Andere Konstruktoren spielen hierfür keine Rolle
#include <iostream>
using namespace std;
class A
{
public:
explicit A(int v1, int v2) : value1(v1), value2(v2) {}
// KEIN expliziter Kopier-Zuweisungs-Operator
A& operator=(bool);
A& operator=(int);
friend ostream& operator<<(ostream& out, const A& a)
{
return out << '[' << a.value1 << ',' << a.value2 << ']';
}
private:
int value1, value2;
};
A& A::operator=(bool b)
{
value1 = b;
value2 = !b;
return *this;
}
A& A::operator=(int v)
{
value1 = v;
value2 = -v;
return *this;
}
int main()
{
A a1(1, 11), a2(2, 22);
cout << "a1=" << a1 << endl;
cout << "a2=" << a2 << endl;
a1 = a2; // Impliziter Kopier-Zuweisungs-Operator
cout << "a1=" << a1 << endl;
a1 = true;
cout << "a1=" << a1 << endl;
a1 = 42;
cout << "a1=" << a1 << endl;
}
Ausgabe:
a1=[1,11]
a2=[2,22]
a1=[2,22]
a1=[1,0]
a1=[42,-42]
3.2.2 Move-Zuweisungs-Operator
- Seit C++11 gibt es in der Sprache "R-Value Referenzen" und "Move-Semantik"
- Zur Move-Semantik gehören der Move-Konstruktor und (hier wichtig) der Move-Zuweisungs-Operator
- Der Move-Zuweisungs-Operator ist derjenige Zuweisungs-Operator, der eine non-const R-Value-Referenz der Klasse erwartet
-
Move-Konstruktor und Move-Zuweisungs-Operator können vom Compiler auch implizit erzeugt werden - folgende Regeln gelten hierbei:
-
Move-Konstruktor und Move-Zuweisungs-Operatoren werden implizit erzeugt, wenn man sie nicht deklariert, und wenn die Klasse:
- Keinen user-deklarierten Kopier-Konstruktor hat,
- keinen user-deklarierten Kopier-Zuweisungs-Operator hat,
- keinen user-deklarierten Move-Zuweisungs-Operator (bzw. Move-Konstruktor) hat,
- keinen user-deklarierten Destruktor hat, und
- wenn alle Elemente der Klasse moveable sind.
-
Move-Konstruktor und Move-Zuweisungs-Operatoren werden implizit erzeugt, wenn man sie nicht deklariert, und wenn die Klasse:
-
Hinweise zur Move-Semantik:
- Bei Klassen ohne Move-Semantik rufen Ausdrücke, die eigentlich Move-Funktionen nutzen würden, dann die Kopier-Funktionen auf
- Eine explizite Move-Funktion verbietet den impliziten Kopier-Konstruktor und den impliziten Kopier-Zuweisungs-Operator
-
Soll eine Klasse Move-Semantik unterstützen, der implizite Move-Zuweisungs-Operator kann aber nicht erzeugt werden (s.o.)
oder er ist nicht optimal - dann muss der User selber einen expliziten Move-Zuweisungs-Operator schreiben.
- Der Move-Zuweisungs-Operator erwartet eine non-const R-Value-Referenz auf ein Objekt der Klasse
- Typischerweise movt er unter Nutzung von "std::move" alle Member - siehe Beispiel
struct MyMoveableClass
{
MyMoveableClass();
MyMoveableClass(MyMoveableClass&&); // Move-Konstruktor
MyMoveableClass& operator=(MyMoveableClass&&); // Expliziter Move-Zuweisungs-Operator
private:
AnotherMoveableClass mMember1;
FurtherMoveableClass mMember2;
};
MyMoveableClass& MyMoveableClass::operator=(MyMoveableClass&& arg)
{
mMember1 = std::move(arg.mMember1);
mMember2 = std::move(arg.mMember2);
return *this;
}
3.3 Operative-Zuweisungs-Operatoren (z.B. "+=")
- Der Compiler macht keine Übertragungen
-
Aus:
- Operator "+"
- Operator "="
-
Wird nicht automatisch
- Operator "+="
-
Alle Operatoren müssen selber definiert werden
- Hier hilft Boost.Operator
- Siehe Kapitel 4
-
Hinweis
- Führe z.B. "+" auf "+=" zurück, nicht umgekehrt
-
Dies ist performanter, denn "+" erzeugt temporäre Objekte[13]
- In C++11 gibt es zwar R-Value Referenzen, die temporäre Objekte vermeiden helfen
- Aber sicherer ist es, es gleich richtig zu machen
- Also den Operator "+" mit "+=" implementieren
#include <iostream>
using namespace std;
struct MyInt
{
MyInt(int v=0) : value(v) {}
MyInt& operator+=(const MyInt&);
MyInt operator+(const MyInt&) const;
friend ostream& operator<<(ostream& out, const MyInt& myint)
{
return out << myint.value;
}
private:
int value;
};
// Operator "+=" auf den "Int-Werten" ausimplementiert
MyInt& MyInt::operator+=(const MyInt& arg)
{
value += arg.value; // Die Implementierung von "+=" basiert auf den "Int-Werten"
return *this;
}
// Operator "+" basiert auf Operator "+="
MyInt MyInt::operator+(const MyInt& arg) const
{
MyInt res(*this);
res += arg; // Die Implementierung von "+" nutzt intern "+=" von "MyInt"
return res;
}
int main()
{
MyInt n0, n1(1), n2(2);
n0 = n1 + n2;
cout << "n0 = n1 + n2 => n0 == " << n0 << endl;
n0 += n2;
cout << "n0 += n2 => n0 == " << n0 << endl;
}
Ausgabe:
n0 = n1 + n2 => n0 == 3
n0 += n2 => n0 == 5
3.4 Prä- und Post-Increment und Decrement Operatoren ("++" und "--")
-
Der Increment-Operator "++" tritt in 2 Varianten auf:
- Prä-Increment "++n"
- Post-Increment "n++"
-
Wie unterscheidet man die Varianten?
- Sind doch beide Male der gleiche unäre Operator "++"
-
Die Post-Variante bekommt einen Int-Dummy-Parameter
- Achtung - den Dummy-Parameter sollte man eigentlich nicht benutzen
- Im Normallfall - Aufruf in Operator-Schreibweise - ist er "0"
- Beim Aufruf in funktionaler Schreibweise - siehe Kapitel 2.4 - kann und muss man ihn explizit angeben
- Analog natürlich auch für den Decrement-Operator "--"
// Achtung - die Operatoren erfuellen so noch nicht die Besonderheiten
// der Prae- und Post-Increment-Operatoren. Das folgt gleich noch.
#include <iostream>
using namespace std;
struct MyInt
{
MyInt(int v=0) : value(v) {}
// Bitte die Rueckgabe-Typen noch ignorieren - die bekommen wir gleich
void operator++(); // Ohne Dummy-Parameter => Prae-Increment, d.h. "++n"
void operator++(int); // Mit Dummy-Parameter => Post-Increment, d.h. "n++"
friend ostream& operator<<(ostream& out, const MyInt& myint)
{
return out << myint.value;
}
private:
int value;
};
void MyInt::operator++()
{
++value;
}
void MyInt::operator++(int)
{
++value;
}
int main()
{
MyInt n(2);
cout << "n == " << n << endl;
++n; // Prae-Increment
cout << "n == " << n << endl;
n++; // Post-Increment
cout << "n == " << n << endl;
}
Ausgabe:
n == 2
n == 3
n == 4
-
Leider erfüllt der obige Code nicht die Besonderheiten der Prä- und Post-Increment-Operatoren
-
Prä-Increment
- Der Wert des "MyInt-Objekts" wird um "eins" erhöht
- Der Ausdruck entspricht dann demselben "MyInt-Objekt" mit dem neuen Wert
-
Post-Increment
- Der Wert des "MyInt-Objekts" wird um "eins" erhöht
- Der Ausdruck entspricht einem "MyInt-Objekt" mit dem alten Wert
-
Prä-Increment
- Um dies zu erreichen, müssen wir die Rückgabe-Typen und die Implementierungen anpassen
-
Fangen wir mit dem Prä-Increment-Operator "++n" an:
- Der Wert des "MyInt-Objekts" wird um "eins" erhöht
- Der Ausdruck entspricht dann demselben "MyInt-Objekt" mit dem neuen Wert
-
Daher muss der Operator das Objekt-Selber ("this") zurückgeben
-
Original-Objekt
- => als Referenz, nicht als Kopie
-
Kein Grund, das Objekt konstant zu halten
- => als Non-Const Referenz, nicht als Const-Referenz
-
Original-Objekt
-
Eine Konsequenz daraus ist:
-
Mehrfache Anwendung müßte möglich sein
- Vergleiche auch Enum Beispiel in Kapitel 2.3.3
-
Dieses Verhalten wäre dann auch äquivalent zu z.B. "int's" in C und C++
- Siehe auch Beispiel
-
Mehrfache Anwendung müßte möglich sein
#include <iostream>
using namespace std;
struct MyInt
{
MyInt(int v=0) : value(v) {}
MyInt& operator++(); // Prae-Increment mit Non-Const-Referenz Rueckgabe
friend ostream& operator<<(ostream& out, const MyInt& myint)
{
return out << myint.value;
}
private:
int value;
};
MyInt& MyInt::operator++()
{
++value;
return *this;
}
int main()
{
MyInt n(2);
cout << "n == " << n << endl;
++n;
cout << "n == " << n << endl;
++++++n;
cout << "n == " << n << endl;
MyInt n2;
n2 = ++n;
cout << "n == " << n << endl;
cout << "n2 == " << n2 << endl;
cout << endl;
int x = 11;
cout << "x == " << x << endl;
++x;
cout << "x == " << x << endl;
++++++x; // Mehrfach-Anwendung von "++n" geht auch bei "int"
cout << "x == " << x << endl;
}
Ausgabe:
n == 2
n == 3
n == 6
n == 7
n2 == 7
x == 11
x == 12
x == 15
-
Und beim Post-Increment-Operator "n++"?
- Der Wert des "MyInt-Objekts" wird um "eins" erhöht
- Der Ausdruck entspricht einem "MyInt-Objekt" mit dem alten Wert
-
Daher muss der Operator ein anderes Objekt mit dem alten Wert zurückgeben
- Das andere Objekt muss eine Kopie des eigentlichen Objekts noch mit dem alten Wert sein
- => Rückgabe per Kopie
-
Das Rückgabe-Objekt sollte gegen Änderungen geschützt sein
- Da Änderungen an diesem Objekt keinen Sinn machen
- Es steht ja nur für einen alten Objekt-Zustand, den es so gar nicht mehr gibt
- => Rückgabe per Const-Kopie
-
Eine Konsequenz daraus ist:
- Mehrfache Anwendung ist nicht möglich
-
Dieses Verhalten ist dann auch äquivalent zu z.B. "int's" in C und C++
- Siehe auch Beispiel
#include <iostream>
using namespace std;
struct MyInt
{
MyInt(int v=0) : value(v) {}
const MyInt operator++(int); // Post-Increment mit Const-Kopie Rueckgabe
friend ostream& operator<<(ostream& out, const MyInt& myint)
{
return out << myint.value;
}
private:
int value;
};
const MyInt MyInt::operator++(int)
{
MyInt tmp(*this);
++value;
return tmp;
}
int main()
{
MyInt n(2);
cout << "n == " << n << endl;
n++;
cout << "n == " << n << endl;
// n++++; // Waere Compiler-Fehler - aufgrund der "Const-Rueckgabe"
MyInt n2;
n2 = n++;
cout << "n == " << n << endl;
cout << "n2 == " << n2 << endl;
cout << endl;
int x = 11;
cout << "x == " << x << endl;
x++;
cout << "x == " << x << endl;
// x++++; // Waere Compiler-Fehler - auch bei "int"
}
Ausgabe:
n == 2
n == 3
n == 4
n2 == 3
x == 11
x == 12
-
Schauen Sie sich bitte beide Implementierungen (Prä und Post) in Ruhe an
-
Sie sehen, dass der Prä-Increment-Operator viel einfacher in der Implementierung ist
- Wert um "einen" erhöhen
- "*this" als Referenz zurückgeben
- Fertig
-
Dem gegenüber ist der Post-Increment-Operator viel komplizierter
- Altes Objekt in Kopie merken
- Wert um "einen" erhöhen
- Kopie per Kopie zurückgeben
- Erste Kopie zerstören
- Zweite Kopie später auch noch zerstören
-
Sie sehen, dass der Prä-Increment-Operator viel einfacher in der Implementierung ist
-
Darum gilt in C++ die Regel:
-
Wenn man nicht explizit das unterschiedliche Verhalten der Operatoren benötigt
- Daher den Rückgabewert in der gleichen Anweisung noch verwendet
-
Dann bevorzuge man den Prä-Operator vor dem Post-Operator
- Denn er ist niemals langsamer, aber er kann performanter sein
-
Wenn man nicht explizit das unterschiedliche Verhalten der Operatoren benötigt
-
Hinweis
- Häufig wird der Compiler beide Kopien vermeiden können
- In meinen Messungen, z.B. bei Iteratoren in Schleifen, habe ich jedenfalls nie einen messbaren Unterschied feststellen können
- Trotzdem - es ist guter Stil, sich nicht auf den Compiler zu verlassen, sondern direkt den richtigen Code zu schreiben
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v;
v.push_back(11);
v.push_back(22);
v.push_back(33);
// Bevorzuge Prae-Operator "++it" wenn moeglich - z.B. bei Iteratoren in Schleifen
for (vector<int>::const_iterator it = v.begin(); it != v.end(); ++it)
{
cout << *it << endl;
}
}
Ausgabe:
11
22
33
- Hinweis: noch besser ist natürlich statt der Iterator-Schleife die Nutzung von Algorithmen oder zumindest der neuen C++11 For-Schleife. Aber das ist ein ganz anderes Thema.
3.5 Logisches "Und" ("&&") und logisches "Oder" ("||")
-
Auch die Operatoren "Logisches Und" "&&" und "Logisches Oder" "||" lassen sich überladen
- Aber das sollte man im Normallfall nicht machen
- Ausnahmen sind z.B. DSELs
-
Warum?
- Die Original-Operatoren haben Kurzschluß-Auswertung
- Dies können