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 überladene Operatoren nicht simulieren
- Sie sind und bleiben Funktions-Aufrufe
- Vor einem Funktions-Aufruf müssen alle Argumente ausgewertet werden
- Der Nutzer-Erwartung wird nicht entsprochen
#include <iostream>
using namespace std;
struct A
{
explicit A(bool v) : value(v) {}
bool operator&&(const A&) const; // Logisches Und
friend ostream& operator<<(ostream& out, const A& a)
{
return out << boolalpha << a.value;
}
private:
bool value;
};
bool A::operator&&(const A& a) const
{
return value && a.value;
}
A fa(bool b)
{
cout << "fa(" << boolalpha << b << ") ";
return A(b);
}
bool f(bool b)
{
cout << "f(" << boolalpha << b << ") ";
return b;
}
int main()
{
cout << "Bool-Test" << endl;
cout << "-> f(false) && f(false) => ";
f(false) && f(false); // Kurzschluss-Auswertung
cout << endl; // => nur ein Aufruf von "f(false)"
cout << "-> f(false) && f(true) => ";
f(false) && f(true); // Kurzschluss-Auswertung
cout << endl; // => nur der Aufruf von "f(false)"
cout << "-> f(true) && f(false) => ";
f(true) && f(false);
cout << endl;
cout << "-> f(true) && f(true) => ";
f(true) && f(true);
cout << endl;
cout << endl;
cout << "A-Test" << endl;
cout << "-> fa(false) && fa(false) => ";
fa(false) && fa(false); // Keine Kurzschluss-Auswertung
cout << endl; // => "fa" wird 2 x aufgerufen
cout << "-> fa(false) && fa(true) => ";
fa(false) && fa(true); // Keine Kurzschluss-Auswertung
cout << endl; // => "fa" wird 2 x aufgerufen
cout << "-> fa(true) && fa(false) => ";
fa(true) && fa(false);
cout << endl;
cout << "-> fa(true) && fa(true) => ";
fa(true) && fa(true);
cout << endl;
}
Ausgabe:
Bool-Test
-> f(false) && f(false) => f(false)
-> f(false) && f(true) => f(false)
-> f(true) && f(false) => f(true) f(false)
-> f(true) && f(true) => f(true) f(true)
A-Test
-> fa(false) && fa(false) => fa(false) fa(false)
-> fa(false) && fa(true) => fa(true) fa(false)
-> fa(true) && fa(false) => fa(false) fa(true)
-> fa(true) && fa(true) => fa(true) fa(true)
- Vielleicht denken Sie: "Na, und? Was soll's? Ich weiss, was passiert - und von daher habe ich alles im Griff!"
- Aber das ist nicht richtig.
- Was macht denn der folgende Code?
type g1(), g2(), g3();
void fct()
{
if (g1() && (g2() || g3()))
{
doit1();
}
}
- Sind "&&" und "||" Orginal-Operatoren oder nicht?
- Wirkt hier die Kurzschluß-Auswertung, oder nicht?
- Wann werden welche Funktionen "g1()", "g2()" und "g3()" aufgerufen?
- Was passiert hier?
-
Diese Fragen sind ohne tieferes Wissen über den Typ "type" nicht beantwortbar.
- Und echter Code ist oft viel komplexer
- Code, der so schwer zu Lesen ist, ist nicht gut - vermeiden Sie ihn
-
Im Sinne der Konsistenz und Lesbarkeit:
- Überladen Sie das logische "Und" bzw. "Oder" im Normallfall nicht!
- Das Überladen ist nur dann akzeptabel, wenn die Kurzschluß-Auswertung keine Rolle spielt
3.6 Adress-Operator ("&")
- Auch der Adress-Operator - der unäre Operator "&" - kann überladen werden
- Im Prinzip ist es ein ganz normaler unärer Operator
-
Aber:
-
Wenn eine Klasse den Adress-Operator überlädt,
dann kann man auf einmal keine Adresse mehr von einem Objekt der Klasse bekommen... - Siehe folgendes Beispiel
-
Wenn eine Klasse den Adress-Operator überlädt,
#include <iostream>
using namespace std;
class A
{
public:
const void* address() const { return this; }
const void* operator&() const { return reinterpret_cast<void*>(1); }
};
class B
{
public:
const void* address() const { return this; }
};
const void* operator&(const B&) { return reinterpret_cast<void*>(2); }
int main()
{
A a;
cout << "a.address(): " << a.address() << endl;
cout << "&a: " << &a << endl;
cout << endl;
B b;
cout << "b.address(): " << b.address() << endl;
cout << "&b: " << &b << endl;
}
Mögliche Ausgabe (nur "möglich" wegen Adress-Abhängigkeit):
a.address(): 0031FB97
&a: 00000001
b.address(): 0031FB8B
&b: 00000002
- Ups - was macht man denn nun, wenn man die Adresse benötigt?
-
Im C++11 ISO Standard[14] ist in §20.6.12.1 die Funktion "addressof" definiert,
die immer die korrekte Adresse eines Objekts liefert - auch wenn der Adress-Operator für das Objekt definiert ist.
- Header <memory>
- template <class T> T* addressof(T& r) noexcept;
- Für ISO C++03 gibt es die gleiche Funktionaliät "boost::addressof" in Boost.Utility[1][2].
#include <iostream>
#include <boost/utility.hpp>
using namespace std;
class A
{
public:
const void* address() const { return this; }
const void* operator&() const { return reinterpret_cast<void*>(1); }
};
class B
{
public:
const void* address() const { return this; }
};
const void* operator&(const B&) { return reinterpret_cast<void*>(2); }
int main()
{
A a;
cout << "addressof(a): " << boost::addressof(a) << endl; // Oder std::addressof in C++11
cout << "a.address(): " << a.address() << endl;
cout << "&a: " << &a << endl;
cout << endl;
B b;
cout << "addressof(b): " << boost::addressof(b) << endl; // Oder std::addressof in C++11
cout << "b.address(): " << b.address() << endl;
cout << "&b: " << &b << endl;
}
Mögliche Ausgabe (nur "möglich" wegen Adress-Abhängigkeit):
addressof(a): 0031FB97
a.address(): 0031FB97
&a: 00000001
addressof(b): 0031FB8B
b.address(): 0031FB8B
&b: 00000002
-
Fazit:
- Seien Sie vorsichtig mit Überladen des Adress-Operators "&"
- Wenn Sie auf "Nummer Sicher" gehen wollen, dann sollten Sie die Adresse eines Objekts immer mit "boost::addressof" bestimmen, und niemals mit dem Operator "&".
3.7 Array-Zugriff bzw. Index-Operator ("[ ]")
- Auch der Index-Operator "[ ]" kann überladen werden
- Er kann einer Klasse "Array-Charakter" geben
-
Beispiele:
- String, Vektor oder Map aus der Standard-Bibliothek
- Er kann nur als Element-Funktion überladen werden
-
Er ist ein binärer Operator, d.h. hat immer zwei Parameter
- Der erste (implizite) Parameter ist immer das Objekt der Klasse selber ("this")
-
Der Typ des zweiten Paramters (das Argument in den eckigen Klammern) ist frei wählbar
- Für den Array-Charakter (Index-Zugriff) wird hier häufig ein "int", "size_t" oder ein ähnlich Integer-Typ gewählt werden.
- Aber auch hier steht einem jeder Typ zur Verfügung.
- Die "std::map" ist hier ein Beispiel, bei der der Index-Operator als Index-Parameter immer den Schlüssel-Typ der Map hat, d.h. bei "map<string, string>" einen "string"
#include <iostream>
#include <string>
using namespace std;
struct A
{
void operator[](int) const;
void operator[](const string&) const;
};
void A::operator[](int n) const
{
cout << "A[" << n << ']' << endl;
}
void A::operator[](const string& s) const
{
cout << "A[\"" << s << "\"]" << endl;
}
int main()
{
A a;
a[1];
a[22];
a[""];
a["C++"];
}
Ausgabe:
A[1]
A[22]
A[""]
A["C++"]
- Eigentlich ist das schon alles...
- Interessant wird der Index-Operator nochmal, wenn man beim Array-Charakter über den Rückgabe-Typ nachdenkt
- Was ist die korrekte Rückgabe, z.B. bei einer Art von "Int-Array"?
- Vielleicht "int"?
#include <iostream>
using namespace std;
struct IntArray
{
IntArray(int v0=0, int v1=0, int v2=0) : a(v0), b(v1), c(v2) {}
int operator[](int) const;
friend ostream& operator<<(ostream& out, const IntArray& a)
{
return out << "IntArray[" << a.a << ',' << a.b << ',' << a.c << ']';
}
private:
int a, b, c;
};
int IntArray::operator[](int idx) const
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int main()
{
IntArray a(11, 22);
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
}
Ausgabe:
a: IntArray[11,22,0]
a[0]: 11
a[1]: 22
a[2]: 0
- Sieht doch gut aus, oder?
- Leider nicht wirklich
- Denn wir können nicht schreiben...
#include <iostream>
using namespace std;
struct IntArray
{
IntArray(int v0=0, int v1=0, int v2=0) : a(v0), b(v1), c(v2) {}
int operator[](int) const;
friend ostream& operator<<(ostream& out, const IntArray& a)
{
return out << "IntArray[" << a.a << ',' << a.b << ',' << a.c << ']';
}
private:
int a, b, c;
};
int IntArray::operator[](int idx) const
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int main()
{
IntArray a(11, 22);
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
a[2] = 33; // Compiler-Fehler - "rvalue" laesst sich nicht schreiben
}
- Compiler-Fehler - und das ist ja auch richtig so
- Wir geben ja eine Kopie des "Array-Wertes" zurück, und die wollen wir nicht beschreiben
- Also geben wir eine Referenz zurück...
- Und vergessen auch nicht, die Funktion "non-const" zu machen...
#include <iostream>
using namespace std;
struct IntArray
{
IntArray(int v0=0, int v1=0, int v2=0) : a(v0), b(v1), c(v2) {}
int& operator[](int) /* kein "const" mehr */;
friend ostream& operator<<(ostream& out, const IntArray& a)
{
return out << "IntArray[" << a.a << ',' << a.b << ',' << a.c << ']';
}
private:
int a, b, c;
};
int& IntArray::operator[](int idx) /* kein "const" mehr */
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int main()
{
IntArray a(11, 22);
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
a[2] = 33; // Super - funktioniert jetzt
cout << endl;
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
}
Ausgabe:
a: IntArray[11,22,0]
a[0]: 11
a[1]: 22
a[2]: 0
a: IntArray[11,22,33]
a[0]: 11
a[1]: 22
a[2]: 33
- Dann sind wir jetzt doch glücklich, oder?
- Nein, denn jetzt haben wir ein Problem mit Const-IntArrays
#include <iostream>
using namespace std;
struct IntArray
{
IntArray(int v0=0, int v1=0, int v2=0) : a(v0), b(v1), c(v2) {}
int& operator[](int);
friend ostream& operator<<(ostream& out, const IntArray& a)
{
return out << "IntArray[" << a.a << ',' << a.b << ',' << a.c << ']';
}
private:
int a, b, c;
};
int& IntArray::operator[](int idx)
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int main()
{
IntArray a(11, 22);
a[2] = 33;
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
cout << endl;
const IntArray ca(111, 222, 333);
cout << "ca: " << ca << endl;
cout << "ca[0]: " << ca[0] << endl; // Compiler-Fehler - Operator [] ist "non-const"
cout << "ca[1]: " << ca[1] << endl; // Compiler-Fehler - Operator [] ist "non-const"
cout << "ca[2]: " << ca[2] << endl; // Compiler-Fehler - Operator [] ist "non-const"
}
- Richtig - wir hatten den Operator "[ ]" "non-const" gemacht, damit wir schreiben können
- Das geht jetzt nach hinten los
- Aber für konstante Objekte benötigen wir ihn "const"
- Zum Schreiben muss er aber "non-const" sein
- Was machen wir jetzt?
- Die Lösung ist ganz einfach:
-
Implementieren wir 2 Index-Operatoren - einen "const" und den anderen "non-const"
- Mit absolut identischer Implementierung - zumindest in diesem Fall
-
Das muss nicht immer so sein - manchmal muss man beide unterschiedlich implementieren,
um die jeweils performanteste Lösung zu bekommen
-
Den Rückgabe-Typ können wir auch wieder individuell anpassen
-
Weiterhin als Referenz für den Non-Const Zugriff
- Zwingend notwendig, da sonst kein Schreiben möglich ist
-
Und als Kopie für den Const-Zugriff
- Entsprechend den Regeln für Funktions-Parameter und Rückgaben[10][11]
- Alternativ wäre noch eine "Const-Referenz" möglich
-
Auf jeden Fall sollte man einen Typ wählen, der
- funktioniert (d.h. überhaupt compiliert), und
- beim versuchten Schreiben zu einem Compiler-Fehler führt
- Sowohl Kopie als auch Const-Referenz erfüllen in diesem Fall beide Kriterien
-
Weiterhin als Referenz für den Non-Const Zugriff
#include <iostream>
using namespace std;
struct IntArray
{
IntArray(int v0=0, int v1=0, int v2=0) : a(v0), b(v1), c(v2) {}
int& operator[](int);
int operator[](int) const;
friend ostream& operator<<(ostream& out, const IntArray& a)
{
return out << "IntArray[" << a.a << ',' << a.b << ',' << a.c << ']';
}
private:
int a, b, c;
};
int& IntArray::operator[](int idx)
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int IntArray::operator[](int idx) const
{
switch (idx)
{
case 0:
return a;
case 1:
return b;
}
return c;
}
int main()
{
IntArray a(11, 22);
a[2] = 33;
cout << "a: " << a << endl;
cout << "a[0]: " << a[0] << endl;
cout << "a[1]: " << a[1] << endl;
cout << "a[2]: " << a[2] << endl;
cout << endl;
const IntArray ca(111, 222, 333);
cout << "ca: " << ca << endl;
cout << "ca[0]: " << ca[0] << endl;
cout << "ca[1]: " << ca[1] << endl;
cout << "ca[2]: " << ca[2] << endl;
}
Ausgabe:
a: IntArray[11,22,33]
a[0]: 11
a[1]: 22
a[2]: 33
ca: IntArray[111,222,333]
ca[0]: 111
ca[1]: 222
ca[2]: 333
-
Noch zwei letzte Hinweise zu diesen Beispielen:
-
Es gibt Leute, Bücher und Artikel, die sagen:
-
int& operator[](int);
- Ist zum Schreiben
-
int operator[](int) const;
- Ist zum Lesen
-
-
Das ist großer Blödsinn!
-
int& operator[](int);
- Wird bei "non-const" Objekten genommen – Lesend und Schreibend
-
int operator[](int) const;
- Wird bei "const" Objekten genommen – und kann nur lesen
-
-
Und ja, man kann mit "const" überladen
-
Es funktioniert: zwei Funktionen, die sich nur durch "const" unterscheiden,
sind für den Compiler beim Aufruf unterscheidbar
-
Es funktioniert: zwei Funktionen, die sich nur durch "const" unterscheiden,
-
Es gibt Leute, Bücher und Artikel, die sagen:
-
Wer mehr zu all dem (Const-Überladen und Schreibe- bzw. Lese-Semantik) wissen will,
dem empfehle ich meinen ausführlichen Artikel zum Thema "Überladen mit const"[15]
-
Fazit:
- Den Index-Operator zu überladen ist ganz leicht
- Wenn man ihn "Lesend" und "Schreibend" benötigt, dann muss man ihn zweimal implementieren
-
Diese Implementierungen unterscheiden sich durch:
- "const" bzw. "non-const"
- Den Rückgabe-Typ
- Und möglicherweise - aber eher seltener - durch die Implementierung
3.8 Element-Zugriff bzw. Pfeil-Operator ("->")
- Auch der Element-Zugriffs-Operator "->" (oder auch Pfeil-Operator genannt) läßt sich überladen
-
Dies kennt man z.B. von Iteratoren oder Smart-Pointern
- Das Überladen des Pfeil-Operators ist also sehr gebräulich
- Hier ein einfaches Beispiel mit einem "shared_ptr" für "string":
int main()
{
shared_ptr<string> ps(new string("Hallo Welt");
cout << ps->length() << endl; // Nutzung des ueberladenen -> Operators
}
-
Aber der Pfeil-Operator ist etwas ungewöhnlich, d.h. in mehreren Details anders als alle anderen Operatoren.
- Im Gegensatz zu anderen Operatoren wird er zweistufig ausgewertet
- Ein Teil der Auswertung findet schon zur Compile-Zeit statt
- Und er ist nur ein "unärer" Operator, obwohl er "binär" aussieht.
- Um das zu verstehen, erinnern wir uns nochmal an den einfachen binären Plus-Operator "+"
// Eine Klasse "A", mit einem freien Operator "+" - irgendwie implementiert
// Und Benutzung von unserem Operator "+"
class A {};
A operator+(const A&, const A&);
A a1, a2;
A a = a1 + a2;
-
Hier ist hoffentlich klar, was passiert.
- Zur Laufzeit entstehen die Objekte "a1" und "a2". Der Operator "+" wird mit ihnen als Argument aufgerufen, und gibt ein neues "A"-Objekt zurück. Ganz einfach.
- Wichtig - hier hat der Operator "+" zwei Operanden, die zur Laufzeit existieren und benutzt werden.
-
Zurück zum Operator "->", aber erstmal für ganz normale Zeiger:
- Ein ganz einfacher Quelltext - fast C.
struct sss
{
int n;
};
sss s = { 1 }; // Ein Objekt "s" der Struktur "sss" erzeugen
sss* p = &s; // Einen Zeiger "p" darauf besorgen und initialisieren
cout << p->n; // Ausgabe des Members "n" ueber den Zeiger "p" mit "->"
-
Was ist hier mit dem Operator "->" ?
- Oh, das ist hier etwas anders als bei "+".
- "p" und "n" sind hier keine Operanden, mit denen zur Laufzeit eine Funktion "->" aufgerufen wird.
- Statt dessen ist der Operator "->" eine Anweisung für den Compiler! (daher was zur Compile-Zeit)
-
Sie besagt:
-
Spreche über den Zeiger links (der die Adresse des Objekts enthält),
den Member "n" an (der ein Element des Objekts sein muss).
-
Spreche über den Zeiger links (der die Adresse des Objekts enthält),
-
Sehen Sie den Unterschied?
- Der Operator "+" wird zur Laufzeit aufgerufen - ist quasi ein normaler Funktionsaufruf.
- Der Operator "->" ist eine Anweisung an den Compiler über die Adresse links das Element rechts anzusprechen.
- Und jetzt schauen wir uns den Pfeil-Operator mal Überladen in einer eigenen Klasse an:
struct A
{
int n;
};
struct APtr
{
APtr(A* arg) : p(arg) {}
A* operator->() { return p; }
A* p;
};
int main()
{
A a = { 2 };
APtr pa(&a);
cout << pa->n;
}
-
Auch hier ist mit dem Ausdruck eigentlich "pa->n" nichts anderes gemeint als im quasi C Beispiel.
- Spreche für das Objekt, dessen Adresse links ist, das Element "n" an.
- Das hat noch nichts mit Laufzeit zu tun, sondern ist eine Anweisung an den Compiler.
-
Nur steht jetzt links keine Adresse, sondern ein Objekt der Klasse "APtr", die kein "n" enthält.
- Und hier kommt jetzt tricky C++ ins Spiel.
-
Im Falle von Klassen interpretiert der Compiler den Operator "->" zweistufig.
- Erstmal wird der Operator "->" für den Operanden links aufgerufen, und dann
- zweitens für das Ergebnis das alte Verfahren ausgeführt.
-
D.h. es wird nicht für die Adresse in "pa" das Element "n" (der Klasse vom Typ "pa's") aufgerufen,
sondern für die von "pa->" zurückgegebene Adresse das Element "n".
Wobei "n" ein Element der Klasse sein muss, deren Adressen-Typ "pa->" zurückgibt.
-
Man kann das folgendermaßen in Pseudo-Code symbolisieren:
- ( pa.operator->() ) -> n
- Genau genommen ist dies gar kein Pseudo-Code, denn der ISO Standard von z.B. C++11[16] schreibt genau dies in §13.5.6 vor: "An expression x->m is interpreted as (x.operator->())->m for a class object x of type T if T::operator->() exists and if the operator is selected as the best match function by the overload resolution mechanism (13.3)."
- Und daher sollte der Operator "->" einen Zeiger zurückgeben
-
Und deshalb ist der Operator "->" auch ein unärer Operator, obwohl er zwichen 2 Operanden steht
- Also im ersten Augenblick nach einem binären Operator aussieht.
- Aber Stopp!
-
Wieso steht da nur sollte?
- "Und daher sollte der Operator "->" einen Zeiger zurückgeben"
- Ohne Zeiger-Rückgabe macht der Operator "->" doch gar keinen Sinn?
-
Doch - Okay, Sinn? - Vielleicht - Also eher nicht - Aber möglich ist es...
- Der Standard sagt nirgendswo, dass nur Zeiger-Rückgaben erlaubt sind
- Erlaubt ist also erstmal alles
-
Und es gibt da zwei Möglichkeiten:
- Wie sinnvoll sie auch immer sein mögen...
- Man gebe irgendwas zurück, und rufe den Operator funktional auf - siehe Kapitel 2.4.
- Man gebe ein Proxy-Objekt zurück, das seinerseits wieder den Operator "->" überladen hat.
- Schauen wir uns beide Ideen mal an.
- 1. Man gebe irgendwas zurück, und rufe den Operator funktional auf
#include <iostream>
using namespace std;
struct B
{
int operator->() { return 5; } // Rueckgabe eines "int"
};
int main()
{
B b;
int x = b.operator->(); // Aufruf in "funktionaler" Schreibweise
cout << x << endl;
}
Ausgabe:
5
- Keine Ahnung, ob das in irgendeiner Situation sinnvoll ist, aber möglich ist es.
-
2. Man gebe ein Proxy-Objekt zurück, das seinerseits wieder den Operator "->" überladen hat.
- Der Standard interpretiert im Fall der Operator-Überladung das "x->m" als "(x.operator->( ))->m".
- Wenn jetzt "(x.operator->( ))" wieder ein Objekt mit überladenem Operator "->" zurückgibt, dann muss der Compiler das rekursiv weiter auflösen.
#include <iostream>
using namespace std;
struct A;
struct AProxy;
struct AProxyProxy
{
AProxyProxy(A* p) : pa(p) {}
A* operator->() { return pa; } // Proxy-Proxy gibt A zurueck
A* pa;
};
struct AProxy
{
AProxy(A* p) : pa(p) {}
AProxyProxy operator->() { return AProxyProxy(pa); } // Proxy gibt ein Proxy-Proxy zurueck
A* pa;
};
struct A
{
A(int x) : n(x) {}
AProxy operator->() { return AProxy(this); } // A gibt nur ein Proxy zurueck
int n;
};
int main()
{
A a(6);
cout << a->n << endl;
}
Ausgabe:
6
- Das Arbeiten mit Proxy-Objekten kann im Einzelfall sinnvoll sein - und funktioniert dann auch mit dem Pfeil-Operator.
3.9 Zeiger-Auf-Element-Zugriff bzw. Pfeil-Stern-Operator ("->*")
-
Der Pfeil-Stern-Operator "->*" ist ein sehr ungewöhnlicher Operator
-
Wie auch sein Mitspieler ".*"
- Der sich übrigens nicht überladen läßt
- Beide werden schon ohne Operator-Überladung nur sehr selten genutzt
- Ich kenne langjährige C++ Programmierer, die sie noch nie benutzt haben bzw. sie gar nicht kennen
-
Wie auch sein Mitspieler ".*"
-
Beide stellen den Zugriff auf ein Element eines Objekt dar
- Sowohl Attribut als auch Element-Funktion
- "->*" über einen Objekt-Zeiger
- ".*" über ein Objekt oder eine Objekt-Referenz
-
Im Gegensatz zur festen Nutzung des Names des Elements, kann hier das Element geändert werden
- Solange es den gleichen Typ hat
-
Im Gegensatz zu einem normalen Zeiger ist hier weiterhin der Objekt-Bezug notwendig
- Und damit ist der Objekt-Bezug auch variabel
- Zur Erinnerung ein Beispiel zur Nutzung, ganz ohne Operator-Überladung
#include <iostream>
using namespace std;
struct A
{
int n;
int m;
void f() { cout << "A::f()" << endl; }
void g() { cout << "A::g()" << endl; }
};
int main()
{
int A::*a_int = &A::n;
void (A::*a_fct)() = &A::f;
A a = { 3, 4 };
A* pa = &a;
cout << a.*a_int << endl; // => 3
cout << pa->*a_int << endl; // => 3
(a.*a_fct)(); // => A::f()
(pa->*a_fct)(); // => A::f()
cout << endl;
a_int = &A::m;
a_fct = &A::g;
cout << a.*a_int << endl; // => 4
cout << pa->*a_int << endl; // => 4
(a.*a_fct)(); // => A::g()
(pa->*a_fct)(); // => A::g()
cout << endl;
A a2 = { 5, 6 };
A* pa2 = &a2;
cout << a2.*a_int << endl; // => 6
cout << pa2->*a_int << endl; // => 6
}
Ausgabe:
3
3
A::f()
A::f()
4
4
A::g()
A::g()
6
6
-
Den Pfeil-Stern-Operator "
->*
" kann man überladen- Sowohl als Element-Funktion, als auch als freie Funktion
-
Den Original-Operator kann man natürlich nicht verändern
- Wie bei keinem Operator, außer "new, delete, new[], ..."
-
Er müßte quasi folgende Signatur für haben:
R operator->*(T*, R T::*)
- Und da hätte ja auch kein Operand Klassen- oder Enum-Typ
-
Die überladenen Pfeil-Stern-Operatoren "
->*
" müssen also ihre ganz eigenständige Signatur haben- Siehe Beispiele im Quelltext
#include <iostream>
using namespace std;
struct A
{
int x;
void operator->*(int n) { cout << "A::->*(" << n << ')' << endl; }
void operator->*(int A::* a) { cout << "A::->*(int A::*) : " << this->*a << endl; }
};
void operator->*(A, bool b)
{
cout << "->*(A, " << b << ')' << endl;
}
int main()
{
cout << boolalpha;
A a = { 4 };
a->*(6);
a->*(true);
int A::*ax = &A::x;
a->*ax;
}
Ausgabe:
A::->*(6)
->*(A, true)
A::->*(int A::*) : 4
-
Und was soll das?
- Das so wahrscheinlich nichts
-
Ich kenne jedenfalls keinen sinnvollen Anwendungsfall, um den Pfeil-Stern-Operatoren "
->*
" so zu überladen
- Und wozu kann man ihn dann überladen?
-
Smart-Pointer und Iteratoren sollten den Pfeil-Stern-Operator "
->*
" überladen, um die Zeiger-Simulation vollständig zu implementieren.- Siehe Beispiel
-
Einfache Smart-Pointer Implementation "APtr" für die Klasse "A" mit Pfeil-Stern-Operator "
->*
"
#include <iostream>
#include <boost/noncopyable.hpp>
using namespace std;
struct A
{
A(int a, int b) : x(a), y(b) {}
int x;
int y;
void fct() { cout << "A::fct(): " << x << ',' << y << endl; }
};
struct APtr : boost::noncopyable
{
APtr(A* pa) : p(pa) {}
~APtr() { delete p; }
A& operator*() { return *p; }
A* operator->() { return p; }
template<class T> T& operator->*(T A::*idx) { return p->*idx; }
A* p;
};
int main()
{
APtr pa = new A(4, 5);
pa->fct(); // Normaler Pfeil-Operator "->"
(*pa).fct(); // Normaler Dereferenzierungs-Operator "*"
int A::*a_int = &A::x;
cout << pa->*a_int << endl; // Nutzung des ueberladenen Operators "->*"
cout << (*pa).*a_int << endl; // Simulation von Operator "->*" mit ".*" - s.u.
a_int = &A::y;
cout << pa->*a_int << endl; // Nutzung des ueberladenen Operators "->*"
cout << (*pa).*a_int << endl; // Simulation von Operator "->*" mit ".*" - s.u.
}
Ausgabe:
A::fct(): 4,5
A::fct(): 4,5
4
4
5
5
-
Häufig wird der Pfeil-Stern-Operator "
->*
" auch in Smart-Pointer- und Iterator-Klassen nicht implementiert, da er durch die Nutzung der normalen Dereferenzierung und Anwendung des Punkt-Stern-Operators ".*
" emuliert werden kann.- Siehe obiges Beispiel
-
Auch die C++11 Standard-Iteratoren und Smart-Pointer implementieren den Pfeil-Stern-Operator nicht.
- Ich weiß leider nicht, ob es dafür einen tieferen Grund gibt - oder sie nur vergessen oder als unwichtig erachtet wurden
3.10 Funktions-Aufruf-Operatoren ("( )")
-
Ein besonderer Operator ist der Funktions-Aufruf-Operator
- Er kann nur als Element-Funktion implementiert werden
- Man kann Objekte von Klassen mit Funktions-Aufruf-Operator wie Funktionen verwenden, d.h. sie quasi "aufrufen"
#include <iostream>
using namespace std;
class A
{
public:
void operator()() const;
};
void A::operator()() const
{
cout << "Funktions-Aufruf-Operator ()" << endl;
}
int main()
{
A a;
a(); // Keine Funktion - Aufruf des Operators "()" fuer das Objekt "a"
}
Ausgabe:
Funktions-Aufruf-Operator ()
-
Im Gegensatz zu allen anderen Operatoren gilt beim Funktins-Aufruf-Operator:
- Die Anzahl an Parametern ist nicht festgelegt - sie kann beliebig sein
- Es dürfen Default-Argumente benutzt werden - wie bei normalen Funktionen
-
Beispiel:
- Mit mehreren überladenen Funktions-Aufruf-Operatoren, d.h. u.a. unterschiedlich vielen Parametern
- Und im letzten Fall auch mit Default-Argument
#include <iostream>
using namespace std;
class A
{
public:
void operator()() const;
void operator()(int) const;
void operator()(bool) const;
void operator()(double, int=5) const;
};
void A::operator()() const
{
cout << "Funktions-Aufruf-Operator ()" << endl;
}
void A::operator()(int n) const
{
cout << "Funktions-Aufruf-Operator (int " << n << ")" << endl;
}
void A::operator()(bool b) const
{
cout << "Funktions-Aufruf-Operator (bool " << b << ")" << endl;
}
void A::operator()(double d, int n) const
{
cout << "Funktions-Aufruf-Operator (double " << d << ", int " << n << ")" << endl;
}
int main()
{
A a;
a(); // Aufruf des Operators "()"
a(4); // Aufruf des Operators "(int)" mit "4"
a(true); // Aufruf des Operators "(bool)" mit "true"
a(2.72); // Aufruf des Operators "(double, int)" mit "2.72, 5"
a(3.14, 6); // Aufruf des Operators "(double, int)" mit "3.14, 6"
}
Ausgabe:
Funktions-Aufruf-Operator ()
Funktions-Aufruf-Operator (int 4)
Funktions-Aufruf-Operator (bool 1)
Funktions-Aufruf-Operator (double 2.72, int 5)
Funktions-Aufruf-Operator (double 3.14, int 6)
-
Man nennt diese Objekte daher auch Funktions-Objekte
- Die Klassen nennt man "Funktions-Objekt-Klassen"
- Aber Achtung - in echt ist dieser Namen vielen Leute zu lang und dann nennen sie häufig auch die Klassen einfach "Funktions-Objekte", obwohl das natürlich falsch ist.
-
Funktions-Objekte sind ein ganz wichtiges Element in C++
- Alle generischen Funktionen können mit Funktionen oder Funktions-Objekten genutzt werden
-
Beispiele:
- STL Algorithmen
- std::thread
- std::function
- Der Oberbegriff von Funktionen und Funktions-Objekten ist "Callable"
-
Im folgenden Beispiel wird ein Vektor mit einem Funktions-Objekt "MyLess()"
und dem STL-Algorithmus "sort" sortiert.
struct MyLess
{
bool operator()(int larg, int rarg) { return larg<rarg; }
};
int main()
{
vector<int> v;
sort(begin(v), end(v), MyLess());
}
-
Warum sind Funktions-Objekte so wichtig?
-
Performanter
- Funktionen sind Optimierungs-Barriere
- Funktions-Objekte halten Typ-Informationen
-
Funktions-Objekte können von Expression-Templates erzeugt werden, z.B. mit:
- "std::bind" oder "boost::bind"
- Boost-Lambda-Library (BLL)
- C++11 Lambda Ausdrücke erzeugen Funktions-Objekte
- Und sie sind mächtiger
-
Performanter
-
Warum sind Funktions-Objekte mächtiger als Funktionen?
-
Können Status tragen
- Attribute
-
Können Hilfs-Funktionen haben
- Andere Element- oder auch Klassen-Funktionen
-
Kann mehrere unabhängige Instanzen davon erzeugen
- Mehrere Objekte erzeugen
-
Benötige keine Parameter-Übergabe zwischen Funktions-Gruppen
- Implizite Parameter Übergabe via „this“ zwischen Element-Funktionen
-
Unterstützten Vererbung und Polymorphie
- Z.B. auch "Template-Method-Pattern"
-
Können Status tragen
3.11 Konvertierungs-Operatoren
todo3.12 User-definierte Literale - Operator ("""")
todo4. Boost.Operator
todo4.1 Einführung
todo4.2 Operator-Konzepte
todo4.3 Composite-Konzepte
todo4.4 Fazit
todo5. Fazit
5.1 Was fehlte?
- Warum nur "Operator-Überladung von A bis fast Z"?
-
Warum nicht "Operator-Überladung von A bis Z"?
- Was fehlte?
-
2 Themen, die aber primär nicht Operator-Überladung sind
- new & delete
- Iteratoren
-
Operatoren "new" & "delete"
- Auch die Operatoren "new" & "delete" bzw. "new[]" und "delete[]" lassen sich überladen
- Sowohl global als auch klassen-spezifisch
- Es lassen sich eigene neue New- und Delete-Operatoren definieren
- Das Thema ist aber eher Speicher-Verwaltung
- Und viel größer, als es Anfangs aussieht
- Daher gehörte es hier nicht hin
-
Iteratoren
- Iteratoren sind ein typisches C++ Beispiel für Operator-Überladung
- Aber Iteratoren enthalten viele weitere Themen und Probleme
- Von daher nur sekundär Operator-Überladung
- Und würde hier den Rahmen und das Thema sprengen
-
Hier nur die Hinweise:
-
Iteratoren zu schreiben ist nicht so einfach
- Wegen der Iterator-Themen, nicht wegen Operator-Überladung
- Wenn, dann nutzen Sie die Hilfe von "Boost.Iterator"[1][2] und "Boost.Operator".
-
Iteratoren zu schreiben ist nicht so einfach
5.2 Wichtig für C++
-
Zuerst sieht Operator-Überladung wie eine nette Spielerei aus
- Für ein paar mathematische Klassen
- Für die Meisten von uns vollkommen überflüssig
-
Aber in C++ geht die Bedeutung viel tiefer, z.B.:
-
Streams
- Aus- und Eingabe
-
Strings und Container
- Intutitive Schnittstelle, z.B. + oder [ ]
-
Iteratoren
- Gleiche Schnittstelle wie Zeiger
- Leichter Einstieg für C Programmierer
- Möglichkeit für generischen Code, der mit Zeigern und Iteratoren arbeitet
-
Smart-Pointer
- Objekte, die wie Zeiger aussehen, aber intelligenter sind
-
Domain Specific Embedded Languages (DSELs)
- Integration von Domainen-Sprachen in C++
- "Compiliert" durch Template-Meta-Programmierung
-
Streams
-
Beispiel für DSEL
-
Boost.Spirit[1][2].
- Objekt-orientiertes rekursives Abstiegs-Parser Generator Framework
- EBNF Regeln und Grammatiken werden direkt in C++ abgebildet
- Der Compiler generiert daraus den eigentlichen Code
- Parser in die Sprache C++ über eine Bibliothek eingebettet
-
Beispiel:
- Parsen einer komma-separierten Liste von Integer-Zahlen
- EBNF: int (',' int )*
- Spirit: int_p >> *(',' >> int_p)
-
Boost.Spirit[1][2].
-
Ohne Operator-Überladung wäre C++ nicht wie es ist
- C++ würde viel von seiner Ausdrucks-Stärke einbüßen
- C++ würde viel von seinen Möglichkeiten einbüßen
- Operator-Überladung paßt einfach zu den anderen Sprach-Merkmalen
- Obwohl es am Anfang nicht so aussieht
5.3 Fazit
-
C++ Operator-Überladung ist eigentlich ganz einfach
- Wenn man normale Funktionen überladen kann, dann kann man auch Operatoren überladen
-
Es gibt nur wenige Grundlagen zu beachten, z.B.:
- Welche Operatoren können überladen werden?
- Es können keine neuen Operatoren definiert werden.
- Priorität und Auswertungs-Reihenfolge liegen fest
- Die Anzahl der Operanden liegt fest (Ausnahme Funktions-Aufruf-Operator)
- Keine Default-Argumente (Ausnahme Funktions-Aufruf-Operator)
- Umsetzung als Element- oder freie Funktion
- usw.
- Und ein paar Besonderheiten bei besonderen Operatoren
- Alles andere ist ganz einfach
-
Problem der Operator-Überladung
-
Über- bzw. Fehl-Benutzung des Features, z.B.:
- Nutzung von Operatoren, die nicht intuitiv sind
- Nutzung von Operatoren, die nicht zur der Implementierung passen
-
Operatoren unintuitiv überladen
- Beispiel: Implementierung einer Subtraktion im Plus-Operator
-
Über- bzw. Fehl-Benutzung des Features, z.B.:
-
Fazit: Operator-Überladung nutzen, wo es sinnvoll ist, z.B.:
- Mathematische Klassen
- Smart-Pointer, Iteratoren,...
- Array-Charakter,...
- Funktions-Objekte
- Anforderungen von außen (z.B. STL mit map oder Algorithmen)
- DSELs
- Usw.
- Und keine Panik - Operator-Überladung ist ganz einfach
6. Links
Hier die Links zu den speziellen Themen aus diesem Artikel:- Die Boost Web-Seite im Internet:
- Ein Einführungs-Artikel in Boost von mir:
- Mein Artikel zum Thema "override in C++11":
-
Mein Artikel zur "Regel der 3":
- Noch nicht veröffentlicht
-
Die "Regel der 3" finden Sie auch gut erklärt im folgenden Buch:
- Scott Meyers, Effektiv C++ programmieren
- 3.Auflage, Item 11
- Addison-Wesley, ISBN 9-783827-31305-8
-
Mein Artikel zu RAII ("resource aquisition is initialisation"):
- Noch nicht veröffentlicht
-
Mein Artikel zu den "Parameter-Übergabe-Konventionen":
- Noch nicht veröffentlicht
-
Vergleiche das Kapitel 2.4 über "Kopien und temporäre Objekte" in meinem Artikel:
- "C++ Performance-Optimierungen"
- http://www.wilkening-online.de/programmieren/c++-performance-optimierungen.html
-
ISO C++ 11 Standard von 2011: ISO/IEC 14882:2011(E)
- Adress-Of in §20.6.12.1 "addressof"
- http://www.open-std.org/jtc1/sc22/wg21/
- Mein Artikel zum Thema "Überladen mit const":
-
ISO C++ 11 Standard von 2011: ISO/IEC 14882:2011(E)
- Überladen des Pfeil-Operators in §13.5.6 "Class member access"
- http://www.open-std.org/jtc1/sc22/wg21/
7. Literatur
C++ Bücher, die sich nur oder zumindest zu einem großen Teil mit Operator-Überladung beschäftigen, kenne ich nicht. Natürlich enthält wohl jedes Lehrbuch zu C++ auch ein Kapitel zur Operator-Überladung - unterm Strich wird sie aber im Verhältnis zu anderen Themen doch immer recht kurz behandelt - daher ja auch dieser Artikel.Trotzdem haben es 2 Bücher in die Literatur-Liste zur C++ Operator-Überladung geschafft.
-
Ein ziemlich altes Buch (1992) von Tom Cargill, dass trotzdem auch heute noch in vielen Bereichen empfehlenswert ist - selbst
wenn natürlich vieles überholt ist. U.a. beschäftige sich ein Kapitel nur mit Operator-Überladung - und deshalb führe ich
es hier auf:
- Tom Cargill
- C++ Programming Style
- Sprache: Englisch
- Verlag: Addison-Wesley Longman
- ISBN: 978-0201514599
- Erscheinungs-Jahr: August 1992
-
Eines der wenigen Boost Bücher auf dem Markt. Und es enthält ein Kapitel über "Boost.Operator":
- Björn Karlsson
- Beyond the C++ Standard Library
- An Introduction to Boost
- Sprache: English
- Verlag: Addison-Wesley Longman
- ISBN: 978-0321133540
- Erscheinungs-Jahr: August 2005
-
2009 hat "pumuckl" in der deutschen C++ Community (http://www.c-plusplus.de)
einen drei-teiligen Artikel über Operator-Überladung in C++ und Boost.Operator veröffentlicht. Der Schwerpunkt seiner Artikel
liegt anders als bei meinem - von daher gab es für mich keinen Grund, meinen nicht doch noch zu schreiben. Ganz im Gegenteil
finde ich, dass wir uns gut ergänzen:
- http://magazin.c-plusplus.de/artikel/Überladung von Operatoren in CPlusPlus (Teil 1)
- http://magazin.c-plusplus.de/artikel/Überladung von Operatoren in CPlusPlus (Teil 2) - Einführung in boost::operators
- http://magazin.c-plusplus.de/artikel/Überladung von Operatoren in CPlusPlus (Teil 3) - boost::operators für Fortgeschrittene
-
Boris Schäling in Highscore hat auch Operator-Kapitel und Boost.Operator todo
wie ist das in seinem buch? todo - Der GotW #4 Artikel von Herb Sutter dreht sich zwar primär um Klassen-Design, aber seine Beispielklasse enthält fast nur Operatoren. Und von daher sind seine Richtlinien fast alle Operator-Überladen-Richtlinien. Also auf jeden Fall lesen - aber das sollte man eh mit allen GotW Artikeln machen...
8. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 16.05.2012
- Initiale Version (Artikel noch nicht vollständig, Kapitel 3 und 4 fehlen vollständig)
-
Version 2
- 22.10.2013
- Kleine Fehlerberichtigungen
- Kapitel 2 um Kapitel 2.5 und 2.6 ergänzt
- Kapitel 3 bis auf Kapitel 3.11 und 3.12 fertig gestellt
- Kapitel 6 und 7 um die zu den Änderungen gehörigen Links ergänzt
-
Version 3
- todo
- Kleine Fehlerberichtigung und Erweiterung bzgl. "std::addressof" in Kapitel 3.6
- todo