16.05.2012
Version: 1
Hinweis - der Artikel ist hier noch nicht vollständig wiedergegeben (Kapitel 3 & 4 fehlen noch). Ein Ausschnitt aus dem vollständigen Artikel entspricht den Folien des Vortrags, den ich am 16.05.2012 auf dem 6-ten C++ User-Treffen in Düsseldorf gehalten habe.
Inhaltsverzeichnis
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
- Sie hat das Stigma des Mysthischen & Undurchschaubaren
- 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 muß 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
-
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 ** 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
- Als Klassen-Funktionen – niemals
- 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
-
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 und eigene "new/delete" Varianten erzeugt werden
-
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).| 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-Increment (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 | - | |
| dynamisc_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 | + | |
| ( ) | 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 überladen. Diese stehen dann für implizite benutzerdefinierte Typ-Umwandlungen - z.B. bei Funktions-Aufrufen - zur Verfügung (siehe Kapitel 3.9). Seit C++11 können diese auch "explizit" gemacht werden.
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& arg) const
{
return Rational(nume*arg.nume, deno*arg.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& arg) const
{
return Rational(nume*arg.nume, deno*arg.deno);
}
Rational Rational::operator*(const Rational& arg) const
{
return Rational(nume*arg.nume, deno*arg.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.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& larg, const Rational& rarg)
{
return Rational(larg.nume*rarg.nume, larg.deno*rarg.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& larg, const Rational& rarg)
{
return Rational(larg.nume*rarg.nume, larg.deno*rarg.deno);
}
Rational operator*(const Rational& larg, const Rational& rarg)
{
return Rational(larg.nume*rarg.nume, larg.deno*rarg.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)
- 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& arg) const
{
return Rational(nume*arg.nume, deno*arg.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& larg, const Rational& rarg)
{
return Rational(larg.nume*rarg.nume, larg.deno*rarg.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 "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, wahrscheinlich 2016)
- 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 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& larg, const Rational& rarg)
{
return Rational(larg.nume*rarg.nume, larg.deno*rarg.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 es lassen sich für Enums Operatoren überladen
- Darum freie Operator-Funktionen
#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
-
Aber 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
-
Beispiel:
- 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[3] vor. Falls Ihr Compiler es noch nicht unterstützt - dann lassen Sie das "override" 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
3. Operator-Besonderheiten
todo3.1 Zuweisungs-Operatoren
todo3.2 Unäre Operatoren
todo3.3 Prä- und Post-Increment und Decrement Operatoren
todo3.4 Logisches "Und" und "Oder"
todo3.5 Adress-Operator
todo3.6 Element-Zugriff bzw. Pfeil-Operator
todo3.7 Index-Operator bzw. Array-Zugriff
todo3.8 Funktions-Aufruf-Operatoren
todo3.9 Konvertierungs-Operatoren
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.:
- Anzahl Operanden
- 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
- Mathematische Klassen
- Smart-Pointer, Iteratoren,...
- Array-Charakter,...
- Funktions-Objekte
- Anforderungen von außen (z.B. STL mit map oder Algorithmen)
- DSELs
- Und keine Panik - Operator-Überladung ist ganz einfach
6. 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
7. Links
Ähnlich wie bei der Literatur, kenne ich keine Web-Seiten, die sich dem Thema "Operator-Überladung" widmen - man gut, daß ich nun diesen Artikel geschrieben habe. Trotzdem gibt es hier einige Links zu ein paar 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"
8. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 16.05.2012
- Initiale Version (Artikel noch nicht vollständig)