[X] Überladung von Operatoren in C++ (Teil 1)
-
pumuckl schrieb:
Beim op++ gibt man in der Regel ein const Foo bzw const Foo& zurück damit Sachen wie
Foo a; ++++a++++;
nicht compilieren.
Hast du n Link wo sowas steht? Natürlich kann man alles const'en was nciht niet- und nagelfest ist, aber muss das? Grade den Präinkrement würde ich nicht const'en, da ein ++++x durchaus vorkommen kann.
Mir war nicht bewusst, dass ++++n funktioniert wobei n ein int ist. ++n++ und n++++ funktionieren aber beim GCC nicht.
Ich habe keinen Link. Es ist auch kein Problem, das oft auftaucht, von daher fällt diese Inkonsistenz quasi nie auf und es gibt kaum Artikel zu dem Thema.
Ist halt "Do as the ints do".
das const bei der Dereferenzierung ist nicht unbedingt nötig, aber da üblicherweise der Iterator/Smartpointer selbst nicht verändert wird hab ichs drangeschrieben. Allgemein sollen das ja nur Beispiele für übliche Implementierungen sein. Dass allgemein zig Möglichkeiten da sind ist ja klar...
Ich bin mir nicht sicher, dass dies allen Lesern klar ist. Wenn du aber nur eine auf Richtlinien runtergespeckte Version geben willst dann ist das aber kein Problem.
Das Inhaltsverzeichnis des zweiten Teil würde ich bereits in den ersten reinpacken. Das lässt dir mehr Flexibilität bei der Gestaltung des zweiten Teils.
Hö? Wenn ich mich mit dem Inhaltsverzeichnis schon festlege macht das doch nix flexibel??
Da scheint irgendwo ein nicht bei mir zu fehlen... Sorry, wir meinen das selbe.
Brainfuck ist eine kleine Sprache die nur 8 Anweisung hat und von daher nicht zu aufwendig zu implementieren ist. Für den Rest ist es 'ne schöne Sprache fürs Kuriositätenkabinett und zu kaum was zu gebrauchen. Da ein Artikel meiner Meinung aber eh zu kurz ist um irgendwas zu implementieren, was man nachher sinnvoll einsetzen kann, kann man durchaus Beispiele wählen die lustig rüber kommen und nicht so ganz ernst gemeint sind.
-
@inkrement: da müsste dann vermutlich das rückgabeobjekt const gemacht werden, da die Änderung eines temporären Objekts ja eh sinnfrei ist.
was die RIchtlinien etc angeht könnte ich im einleitenden kapitel nochmal genauer drauf eingehen dass grundsätzlich viele Parameter- und Rückgabetypen denkbar, aber nicht unbedingt sinnvoll sind und ich in den Beispielen das typische "do as the ints do" angebe, soweit sinnvoll.
-
Änderungsnotiz:
- op &=, |=, ^=, <<=, >>= -Absatz eingebaut
- op ->* -Absatz eingebaut (war schwer da was zu finden, bitte durchlesen und kommentieren!)
- Anmerkung zur Auswertungsreihenfolge bei op,
- in Einleitung zu Kapitel 3 nochmal explizit drauf hingewiesen dass es sich um übliche Implementierungen im Sinne der üblichen Semantik handelt.
- in Kap 1 genauer beschrieben, dass es diverse mögliche Parameter- und Rückgabetypen gibt.
- Inhaltsverz. Teil 2 rausgenommen
-
Änderungsnotiz:
- Rückgabe von op* und op->* auf Referenz geändert.
-
nachdem ich meine letzten Zweifel bezüglich op->* weitgehend beseitigen konnte setze ich die ganze Geschichte mal auf [R]
-
Änderungsnotiz: Formatierung einer Überschrift, Typos
-
Überladung von Operatoren in C++ (Teil 1)
Operatorüberladung ist ein häufig benutztes Feature in C++. Sie dient dazu, übliche Operationen wie zum Beispiel die Addition übersichtlich und leicht lesbar in den Quelltext einzubauen. Ich möchte mit diesem Artikel einen Überblick darüber geben, wann und wie man in C++ Operatoren überladen kann, darf und sollte. Im Anschluss werden alle möglichen Operatoren genauer untersucht.
Es wird vermutlich noch einen zweiten Teil und evtl. sogar einen dritten Teil geben. Mögliche Themen sind unter anderem ein paar Beispiele zur Operatorüberladung aus den Boost-Bibliotheken zu besprechen, genauer auf die Überladungen der Speicherverwaltungsoperatoren new und delete einzugehen und das sogenannte "Safe Bool Idiom" zu besprechen, das auf Operatorüberladung basiert und im Zusammenhang mit Objekten in boolschem Kontext eine Rolle spielt. Für weitere Themenvorschläge rund um die Operatorüberladung bin ich offen :).Inhalt
Teil 1
- 1. Einführung
- 2. Die Überladung
- 2.1 Wann sollte man Operatoren überladen?
- 2.2 Wie kann man Operatoren überladen?
- 2.3 Welche Operatoren kann man überladen?
- 3. Die Operatoren
- 3.1
operator=
- 3.2
operator+
,-
,*
,/
,%
- 3.3
operator+
,-
(unär) - 3.4
operator<<
,>>
- 3.5
operator&
(binär),|
,^
- 3.6
operator+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
- 3.7
operator&=
,|=
,^=
,<<=
,>>=
- 3.8
operator==
,!=
- 3.9
operator<
,<=
,>
,>=
- 3.10
operator++
,--
- 3.11
operator()
- 3.12
operator[]
- 3.13
operator!
- 3.14
operator&&
,||
- 3.15
operator*
(unär) - 3.16
operator->
- 3.17
operator->*
- 3.18
operator&
(unär) - 3.19
operator,
- 3.20
operator~
- 3.21
operator new
,new[]
,delete
,delete[]
- 3.22 Konvertierungsoperatoren
1. Einführung
In vielen Programmiersprachen gibt es Operatoren. Die wohl am häufigsten vorkommenden Operatoren sind die Zuweisung (’
=
’ oder ’:=
’ oder andere) und die arithmetischen Operatoren (’+
’, ’-
’, ’*
’ und ’/
’). In den meisten Sprachen sind diese Operatoren, vor allem die arithmetischen, auf einen bestimmten Satz eingebauter Datentypen beschränkt.Zum Beispiel ist in Java die Addition mit der Schreibweise "
a + b
" nur für die eingebauten primitiven Datentypen (Ganzzahlen und Fließkommzahlen) sowie für Strings möglich. Definiert man sich eine eigene Klasse mathematischer Objekte, z.B. Matrizen, dann kann man zwar auch eine Methode zu deren Addition implementieren, kann sie dann aber nicht mit dem Operator aufrufen sondern muss z.B. etwas wie "Matrix.add(a, b)
" schreiben.Diese Einschränkung gibt es in C++ nicht: man kann fast alle in C++ bekannten Operatoren überladen. Machbar ist dabei sehr viel: die Typen der einzelnen Operanden und die Rückgabetypen sind frei wählbar, die einzige Voraussetzug ist, dass mindestens ein Operand ein selbstdefinierter Datentyp ist. Es ist also nicht möglich, für die eingebauten Datentypen neue Operatoren zu definieren oder bereits vorhandene Operatoren zu überschreiben.
2. Die Überladung
2.1 Wann sollte man Operatoren überladen?
Die etwas allgemein gefasste Antwort ist: Immer dann, wenn es sinnvoll ist. Sinnvoll ist eine Operatorüberladung dann, wenn die Benutzung des Operators danach intuitiv geschehen kann und keine Überraschungen liefert. Allgemein gilt der Leitsatz "Do as the ints do.": Das Verhalten von überladenen Operatoren mit eigenen Datentypen sollte dem Verhalten dieser Operatoren mit den eingebauten integralen Typen ähneln.
Wie immer bestätigen Ausnahmen die Regel, daher können auch Operatoren in völlig anderen Kontexten überladen werden, wenn das resultierende Verhalten und die richtige Benutzung ausreichend dokumentiert sind. Eine allgemein geläufige Ausnahme ist die Überladung der shift-Operatoren (’<<
’ und ’>>
’) für die Streams der Standarbibliothek. Weitere Beispiele folgen in Kapitel 4.2.Ein paar Beispiele für gute und schlechte Operator-Überladungen:
Die oben angeführte Addition für Matrizen ist ein Beispiel wo man ohne große Überlegungen die Überladung des entsprechenden Operators vornehmen kann. Matrizen sind mathematische Objekte, die Addition ist eine fest definierte Operation, daher können bei richtiger Implementierung keine große Überraschungen auftauchen und jeder weiß sofort was es bedeutet, wenn er im Quelltext etwas liest wieMatrix a, b; Matrix c = a + b;
Natürlich sollte der Operator dann nicht so implementiert werden, dass das Ergebnis von
a + b
das Produkt der beiden Matrizen oder etwas noch seltsameres ist.Ein Beispiel schlechter Operatorüberladung ist die Addition von zwei Spieler-Objekten in einem Spiel. Was könnte der Designer der Klasse damit bezwecken wollen? Betrachtet er die Spieler als mathematische Objekte und das Ergebnis ist ein neuer Spieler? Wie sieht dieser dann aus? Oder ist das Ergebnis der Addition eine Gruppe, bestehend aus den beiden Spielern? Allein die Fragen zeigen schon, warum die Überladung der Addition für die Spieler-Klasse schlecht wäre: Man weiß nicht so recht was die Operation bewirkt und schon das allein macht sie so gut wie unbrauchbar.
Ein Bespiel wo Operatorüberladung kontrovers betrachtet wird, ist die Addition von Elementen zu Containern oder das Zusammenfügen von Containern mit dem Additionsoperator. Dass die Addition von Containern diese zusammenfügen soll mag offensichtlich sein. Nicht offensichtlich ist jedoch, wie dieses Zusammenfügen geschieht: bei sequenziellen Containern stellt sich die Frage, ob die Summe zweier sortierter Container wieder sortiert ist, bei der Summe von
std::map
s wäre nicht offensichtlich, was passiert wenn ein Key in beiden Maps vorkommt usw. Aus dem Grund benutzt man für derartige Operationen meist Funktionen mit sprechenderen Namen wieappend()
,merge()
etc. Allerdings gibt es in der Bibliothekboost::assign
den Operator ’+=
’ für Container, der dem Container ein oder mehrere Elemente hinzufügt.2.2 Wie kann man Operatoren überladen?
Operatorüberladung ist allgemein eine normale Funktionsüberladung, wobei die Funktionen spezielle Namen haben. Diese Namen beginnen alle mit dem Schlüsselwort "
operator
", gefolgt von dem Token für den jeweiligen Operator. Bei den Operatoren, deren Token nicht aus Sonderzeichen bestehen, also den Typkonvertierungsoperatoren und den Operatoren zur Speicherverwaltung (new
,delete
& Co.), muss zwischen dem Schlüsselwort und dem Operatortoken mindestens ein Whitespace stehen (Leerzeichen, Tab, Zeilenumbruch, ...), bei den anderen Operatoren kann der Whitespace entfallen.Die meisten Operatoren können sowohl als Methode einer Klasse als auch als freie Funktionen überladen werden. Es gibt aber eine Handvoll Ausnahmen, die nur als Klassenmethoden überladen werden dürfen. Wird eine Überladung als Klassenmethode deklariert, dann ist der erste Operand immer vom Typ der Klasse (nämlich
*this
), lediglich der zweite Operand wird in der Parameterliste angegeben. Außerdem sind Operatormethoden mit Ausnahme vonoperator new
undoperator delete
grundsätzlich nicht statisch. Zum Einen ermöglicht die Deklaration als Klassenmethode dem Operator direkten Zugriff auf die privaten Attribute und Methoden der Klasse, zum Anderen sind damit implizite Konvertierungen des ersten Arguments ausgeschlossen. Letzteres kann zwar erwünscht sein, ist es im Allgemeinen aber nicht, daher wird z.B.operator+
meistens als freie Funktion überladen. Beispiel:class Rational { public: Rational(int i); //Konstruktor für Konvertierungen von Ganzzahlen Rational operator+(Rational const& rhs) const; }; Rational a, b, c; int i; a = b + c; //ok, keine Konvertierung nötig a = b + i; //ok, implizite Konvertierung des zweiten Arguments a = i + c; //FEHLER: erstes Argument kann nicht konvertiert werden, da operator+ keine freie Funktion ist
Die Signaturen von Operatorüberladungen und ob sie als Klassenmethode oder als freie Funktion implementiert werden, unterliegen (abgesehen von der Anzahl der Argumente) nur wenigen Einschränkungen, so dass es z.B. durchaus möglich wäre eine Addition eines Kreises zu einem Rechteck zu definieren, die eine Pyramide ergibt. Allerdings gibt es für viele Operatoren allgemein gebräuchliche Vorgehensweisen die im Folgenden genauer beschrieben werden.
Eine allgemeine Richtlinie, für die es wie immer natürlich auch Ausnahmen gibt, ist fogende: Wenn unäre Operatoren nicht als Klassenmethode überladen werden, ist eine implizite Konvertierung des Arguments möglich, was meist ein unerwartetes Feature ist. Anders herum ist es häufig erwünscht, dass bei binären Operatoren eines der Argumente implizit in den eigentlichen Typ konvertiert werden kann, auf dem der Operator wirkt. Damit eine Konvertierung des ersen Arguments möglich ist, muss der binäre Operator als freie Funktion überladen werden. Das gilt allerdings nicht für operator+= und ähnliche, da hier das erste Argument modifiziert werden soll und daher eine Konvertierung in ein temporäres Objekt sinnfrei wäre. Zusamengefasst lautet die Richtlinie also: unäre Operatoren und die Operatoren der "X="-Familie als Klassenmethode, alle anderen binären Operatoren als freie Funktion überladen.
2.3 Welche Operatoren kann man überladen?
Es können alle C++-Operatoren überladen werden, mit folgenden Ausnahmen und Einschränkungen:
- Es können keine neuen Operatoren definiert werden wie z.B. ein Exponential-Operator ’
**
’ oder ähnliches. - Folgende Operatoren dürfen nur als Klassenmethoden überladen werden: ’
=
’, ’->
’, ’()
’, ’[]
’, die Konvertierungsoperatoren sowie klassenspezifische Operatoren zur Speicherverwaltung. - Folgende Operatoren dürfen gar nicht überladen werden: ’
?:
’, ’::
’, ’.
’, ’.*
’ , ’->*
’,typeid
,sizeof
und die C++-Cast-Operatoren. - Die Anzahl der Operanden, die Priorität und Assoziativität der einzelnen Operatoren ist in der Sprache festgelegt und kann nicht verändert werden.
- Mindestens ein Operand muss ein nutzerdefinierter Datentyp sein (
class
oderstruct
,typedef
s auf andere Typen zählen nicht als eigenständiger Typ).
3. Die Operatoren
Im Folgenden werden die Operatoren einzeln oder in Gruppen vorgestellt. Zu jedem Operator bzw. jeder Operatorfamilie gibt es eine übliche Semantik, d.h. was man allgemein vom Verhalten des Operators erwartet. Meist entspricht das dem bereits erwähnten "do as the ints do" bzw. bei den Dereferenzierungs- und Pointerzugriffs-Operatoren "do as the pointers do". Beim Überladen sollte man sich normalerweise an diese Semantik halten, damit die Anwender der Klasse keine Überraschungen erleben. Des Weiteren wird, soweit existent, ein Beispiel für eine übliche Deklaration und übliche Implementierungen gegeben, die dieser allgemein üblichen Semantik gerecht werden sowie auf etwaige Besonderheiten eingegangen. Bei den Codebeispielen ist
X
als nutzerdefinierter Typ zu interpretieren, für den die entsprechenden Operatoren implementiert werden.T
bezeichnet einen beliebigen Typen (nutzerdefiniert oder eingebaut). Die Argumente für binäre Operatoren werden im Folgenden mitlhs
bzw.rhs
(left-hand-side und right-hand-side) bezeichnet.3.1 operator=
Semantik: Zuweisung
a = b
. Der Wert bzw. Zustand von b wird nach a kopiert.
Übliche Deklaration:X& X::operator= (X const& rhs);
Der Rückgabewert des Operators ist das Objekt, dem etwas zugewiesen wird. Da kein neues Objekt erzeugt wird, reicht eine Referenz als Rückgabewert. Dies ermöglicht Kettenzuweisungen á la
a = b = c
.
Andere Argumenttypen sind unüblich, da bei einer möglichen Zuweisungx = t
mit verschiedenen Typen X und T meist auch eine implizite Umwandlung von T nach X vorhanden ist, so dass deroperator=(X const&)
ausreichend ist.
Übliche Implementierungen:X& X::operator= (X const& rhs) { if (this != &rhs) //oder if (*this != rhs) { /* kopiere elementweise oder:*/ X tmp(rhs); //Copy-Konstruktor swap(tmp); } return *this; //Referenz auf das Objekt selbst zurückgeben }
Die gezeigte Implementation mit dem Copy-Konstruktor und dem Aufruf einer separat definierten swap-Routine wird in der Praxis häufig eingesetzt, vor allem wenn die Klasse über dynamisch allokierten Speicher verfügt. Die swap-Routine zur Vertauschung von Objekten bzw. ihren Zuständen kann gerade bei dynamisch allokiertem Speicher in den Objekten eine Menge Kopierarbeit und Speichermanagement sparen, indem einfach die Pointer auf den Speicher zwischen den beiden Objekten ausgetauscht werden.
Der Test auf Gleicheit der beiden Argumente wird ab und zu benutzt, um die Kopierarbeiten zu sparen, vor allem wenn es sich um händische Implementierungen und tiefe Kopien handelt. In der Regel ist aber eine copy&swap-Implementierung nach Möglichkeit vorzuziehn.
Besonderheiten:operator=
darf nur als Klassenmethode implementiert werden.
Deroperator=
fällt unter die "Regel der großen Drei". Diese besagt, dass wenn man einen Destruktor, Copy-Konstruktor oder Zuweisungsoperator für eine Klasse definieren muss, höchstwahrscheinlich auch die anderen beiden definiert werden müssen, z.B. um Speicher und andere Ressourcen korrekt zu verwalten.
Für Klassen für die man keinenoperator=
explizit überlädt, macht der Compiler das automatisch, sobald man eine Zuweisung verwendet. Der generierte Zuweisungsoperator hat dann die oben beschriebene Signatur und weist jedes einzelne Attribut des Quellobjektes dem entsprechenden Attribut des Zielobjektes zu. Um dies zu vermeiden kann man denoperator=
alsprivate
deklarieren und die Implementierung weglassen. Außerdem schlägt die automatische Generierung fehl, sobald die Klasse über konstante, nichtstatische Attribute verfügt.3.2 operator+, -, *, /, %
Semantik: Addition, Subtraktion, Multiplikation, Division, Modulo. Es wird ein neues Objekt mit dem Ergebniszustand erzeugt. Die folgenden Ausführungen gelten jeweils analog für
-
,*
,/
und%
.
Übliche Deklaration:X operator+(X const& lhs, X const& rhs);
Übliche Implementierung:
X operator+(X const& lhs, X const& rhs) { /* Erzeugen eines neuen Objektes dessen Attribute gezielt einzeln gesetzt werden. Oder: */ X tmp(lhs); //Kopie des linken Operanden tmp += rhs; //Implementierung mittels des +=-Operators return tmp; }
In vielen Fällen macht das Vorhandensein des
operator+
auch die Existenz desoperator+=
sinnvoll, um die kürzere Schreibweisea += b
an Stelle vona = a + b
zu ermöglichen. Um das Verhalten beider Operatoren konsistent zu halten ist es daher üblich,operator+
im Sinne vonoperator+=
zu implementieren (siehe Beispiel). Um eine implizite Typumwandlung des ersten Operanden zu ermöglichen, werden die binären arithmetischen Operatoren üblicherweise als freie Funktionen definiert.
Wird der Operator nicht mittelsoperator+=
implementiert, benötigt er meist Zugriff auf private Attribute von X. In dem Fall wird er häufig als friend der Klasse deklariert oder greift auf eine öffentliche Methode der Klasse zu, die die eigentliche Operation ausführt:X X::plus(X const& rhs) const; //Implementiert die Addition X operator+(X const& lhs, X const& rhs) { return lhs.plus(rhs); }
3.3 operator+, - (unär)
Semantik: Vorzeichen, der unäre
operator+
wird in dem Sinne eher selten gesichtet. Generell gilt aber für beide das Gleiche.
Übliche Deklaration:X X::operator-() const;
Da man bei Vorzeichen vor einem bestimmten Objekt meistens keine Konvertierung in eine andere Klasse haben möchte/erwartet, wird operator- als Klassenmethode implementiert. Dies gilt für die meisten anderen unären Operatoren auch. Das Ergebnis des Vorzeichenwechsels ist ein neues Objekt, das Ursprungsobjekt bleibt unverändert.
Übliche Implementierung:
Erstellen einer Kopie des Objektes und ändern der vorzeichensensitiven Attribute des neuen Objektes.3.4 operator<<, >>
Semantik: Die Shift-Operatoren
<<
und>>
bedeuten für eingebaute Typen eine bitweise Verschiebung der internen Darstellung. Bei integralen vorzeichenlosen Typen bedeutet das eine Multiplikation bzw. ganzzahlige Divison mit 2. Die Überladung als echte Shift-Operatoren kommt nur selten vor, daher wird diese Möglichkeit hier nicht weiter beschrieben.
Im Zusammenhang mit den Streams der Standardbibliothek sind die beiden Operatoren als Input/Output-Operatoren bzw. Streaming-Operatoren überladen.
Übliche Deklaration:std::ostream& operator<<(std::ostream& lhs, X const& rhs); std::istream& operator>>(std::istream& lhs, X& rhs);
Man beachte, dass bei der Ausgabe das X-Objekt im allgemeinen nicht verändert wird, während beim Einlesen die Attribute des Objektes beschrieben werden und es daher nicht konstant sein darf.
Die Rückgabe des Streamobjektes ermöglicht die übliche Verkettung von Eingabe bzw. Ausgabe wie instd::cout << a << b;
Da der linke Operand das Streamobjekt ist und die Klassen der Standardbibliothek nicht nachträglich erweitert werden können, müssen die Streaming-Operatoren als freie Funktionen definiert werden.
Übliche Implementierung:
Die für die Ein/Ausgabe wichtigen Attribute werden in das übergebene Streamobjekt geschrieben bzw. daraus gelesen. Am Ende wird wieder eine Referenz auf das Objekt zurückgegeben. Wenn die Operatoren Zugriff auf private Elemente der Klasse X benötigen, müssen sie entweder alsfriend
von X deklariert werden oder die Arbeit an eine public Methode delegieren, z.B.std::ostream& X::outputToStream(std::ostream&) const; std::ostream& operator<<(std::ostream& lhs, X const& rhs) { return rhs.outputToStream(lhs); }
3.5 operator& (binär), |, ^
Semantik: Bitweises Und, Oder, exklusives Oder .
Wie bei den Shift-Operatoren ist es eher unüblich, die Bitlogik-Operatoren zu überladen. Zuweilen werden sie für eine Art logische Verknüpfung bereitgestellt, deren Implementierung aber stark von der Programmlogik und dem Klassendesign abhängig ist.3.6 operator+=, -=, *=, /=, %=, &=, |=, ^=
Semantik:
a += b
ist gleichbedeutend mita = a + b
, allerdings ohne dass der Ausdruck a zweimal ausgewertet wird. Analoges Verhalten gilt für die anderen Operatoren.
Übliche Deklaration:X& X::operator+=(X const& rhs);
Da das Ziel der Operation eine Veränderung des linken Operanden ist, macht es wenig Sinn den Operator für implizite Typumwandlungen als freie Funktion zu definieren; die Veränderung würde lediglich das temporäre Objekt betreffen das aus der Typumwandlung entsteht. Der rechte Operand bleibt unverändert.
Übliche Implementierung:
Da der Operator eine Methode der Klasse ist, kann er direkt die Attribute des Objektes verändern wie es die Operation erfordert. Am Ende wird eine Referenz auf*this
zurückgegeben.3.7 operator&=, |=, ^=, <<=, >>=
Semantik: Wie in 3.6 für die entsprechenden Operatoren, allerdings werden die Shift-Operatoren nur als solche und nicht als Streaming-Operatoren überladen. Ebenso wie die zugehörigen Grundoperatoren werden diese Operatoren eher selten überladen.
3.8 operator==, !=
Semantik: Test auf Gleichheit bzw. Ungleichheit.
Übliche Deklaration:bool operator==(X const& lhs, X const& rhs); //operator!= analog
Wegen möglicher impliziter Konvertierungen des ersten Arguments werden die Operatoren als freie Funktionen definiert, die Argumente bleiben unverändert.
Übliche Implementierung:
Der Test auf Gleichheit ist eine Frage der Objektidentität. Zwei Objekte können als gleich angesehen werden, wenn die Attribute die ihren Zustand definieren gleich sind oder in einer restriktiveren Sicht nur dann, wenn es sich um das selbe Objekt handelt. Beide Sichtweisen kommen vor und es hängt von der Programmlogik ab, welche angewandt werden muss. Die restriktive Sicht ist leicht zu implementieren, für den Fall dass der Adressoperator nicht überladen ist:{ return &lhs == &rhs; }
Der allgemeinere Test auf gleichen Zustand wird durchgeführt, indem die für den Zustand relevanten Attribute beider Objekte einzeln verglichen werden. Sind diese Attribute privat, muss der Operator als friend von X deklariert werden oder auf eine Methode von X zurückgreifen, die den Vergleich durchführt.
Deroperator!=
wird allgemein durch einfache Negation desoperator==
implementiert:bool operator != (X const& lhs, X const& rhs) { return ! (lhs==rhs); }
Da die Negation eines boolschen Wertes nicht überladen werden kann, bleibt das Verhalten der beiden Operatoren dadurch immer konsistent.
3.9 operator<, <=, >, >=
Semantik: Test auf Erfüllen einer Ordnungsrelation (kleiner, größer etc.). Die folgenden Ausführungen gelten analog für alle vier Operatoren.
Übliche Deklaration:bool operator<(X const& lhs, X const& rhs);
Wegen möglicher impliziter Konvertierungen des ersten Arguments werden die Operatoren als freie Funktionen definiert, die Argumente bleiben unverändert.
Übliche Implementierung:
Meistens reicht es, die Ordnungsrelation in einem Operator, z.B.operator<
, zu implementieren und die anderen Operatoren darauf und aufoperator==
zugreifen zu lassen.3.10 operator++, --
Semantik:
a++
: Postinkrement, erhöht den Wert von a um eins und gibt den Wert von a vor der Erhöhung zurück. Im Gegensatz dazu gibt das Präinkrement++a
den Wert von a nach der Erhöhung zurück. Analogoperator--
als Post- bzw. Prädekrement (Erniedrigung).
Übliche Deklaration:X& X::operator++(); //Praeinkrement const X X::operator++(int); //Postinkrement
Die Deklaration der Dekrementoperatoren geschieht analog. Die Angabe eines formalen int-Parameters für die Postfix-Operatoren dient lediglich der Unterscheidung, das Argument darf nicht ausgewertet werden. Die Notwendigkeit unterschiedlicher Rückgabetypen als Referenz bzw. eigenes Objekt entsteht aus der Semantik und der entsprechenden Implementierung der Operatoren. Das Postinkrement gibt ein konstantes Objekt zurück, da es unsinnig wäre das temporäre Rückgabeobjekt zu ändern.
Übliche Implementierung:
Der Präinkrementoperator wird implementiert, indem die relevanten Attribute entsprechend verändert werden und anschließend eine Referenz auf*this
zurückgegeben wird.
Der Postinkrementoperator wird meist mittels des Präinkrements implementiert, seine Semantik macht es im Normalfall nötig dass vor der Erhöhung eine Kopie gemacht wird, die am Ende zurückgegeben wird:X X::operator++(int) { X tmp(*this); //Kopier-Konstruktor ++(*this); //Inkrement return tmp; //alten Wert zurueckgeben }
3.11 operator()
Semantik: Ausführen eines Funktionsobjekts. Die Definition eines
operator()
sorgt dafür, dass Objekte der Klasse wie Funktionen ausgeführt werden können.
Deklaration:Foo X::operator()(Bar b, Baz b2);
Die Anzahl und Typen der Parameter sowie die Art des Rückgabetyps hängen von der Semantik der gewünschten Funktionalität ab und sind frei wählbar.
Besonderheit: Deroperator()
kann nur als Klassenmethode definiert werden.3.12 operator[]
Semantik: Arrayzugriff, indizierter Zugriff. Beispiele sind
std::vector
,std::map
,boost::array
.
Deklaration: Der Typ des Parameters ist bei der Deklaration frei wählbar, ebenso der Rückgabetyp. Häufig wird für Containerzugriff eine const Version und eine non-const Version des Operators überladen:T_return& X::operator[](T_index const& index); const T_return& X::operator[](T_index const& index) const;
Dies spiegelt wieder, dass bei konstanten Containern auch die Inhalte nicht veränderlich sein sollen.
Besonderheit: Deroperator[]
kann nur als Klassenmethode definiert werden.3.13 operator!
Semantik: Negation, Verneinung. Der
operator!
impliziert einen booleschen Kontext, im Gegensatz zum Komplement-Operaroperator~
. Intuitiv könnte man erwarten, dass wenn die Negation eines Objektes möglich ist, das Objekt selbst auch in einem boolschen Kontext benutzt werden kann. Dies ist allein mitoperator!
allerdings nur durch die unintuitive doppelte Verneinung möglich, z.B.if (!!x)
. Eine Lösung für die Problematik des boolschen Kontextes und die damit zusammenhängenden Probleme bietet das sogenannte "Safe Bool Idiom", das die Überladung desoperator!
mit der üblichen Semantik überflüssig macht.
Übliche Deklaration:bool X::operator!() const;
Wie alle unären Operatoren sollte deroperator!
als Klassenmethode definiert werden.3.14 operator&&, ||
Semantik: Logisches Und, logisches Oder. Die logischen Operatoren existieren für eingebaute Typen nur für die boolschen Werte. Für diese sind die Operatoren als Kurzschlussoperatoren implementiert, das heißt wenn nach Auswertung des ersten Operanden das Ergebnis klar ist, wird der zweite Operand nicht ausgewertet.
Bei Überladung der logischen Operatoren mit eigenen Datentypen wird das Kurzschlussverhaltennicht
mit übernommen. Daher wird allgemein davon abgeraten, sie zu überladen, zumal für die Verwendung der eigenen Klasse in boolschem Kontext das "Safe Bool Idiom" verwendet werden kann.
Deklaration: Sollten die logischen Operatoren dennoch überladen werden, geschieht das wie bei den meisten binären Operatoren tendenziell als freie Funktion, um implizite Konvertierungen des ersten Arguments zu ermöglichen.3.15 operator* (unär)
Semantik: Dereferenzierung von Pointern. Dieser Operator wird hauptsächlich für Smartpointer- und Iteratorklassen überladen.
Übliche Deklaration:T& X::operator*() const;
Wie für unäre Operatoren üblich, wird der Dereferenzierungsoperator meist als Klassenmethode implementiert. Der Rückgabetyp ist der Typ der Klasse, auf den der Iterator bzw. Pointer zeigt. Häufig haben Iteratorklassen und Smartpointerklassen ein
typedef
namensvalue_type
auf diesen Typ. Der Pointer bzw. Iterator selbst wird bei der Dereferenzierung nicht verändert.
Übliche Implementierung: Smartpointer und Iteratoren werden häufig mittels normaler Pointer oder anderer Pointer- bzw. Iteratorklassen implementiert. Die Dereferenzierung erfolgt dann einfach über die Dereferenzierung dieses eingebetteten Pointers.3.16 operator->
Semantik: Attributzugriff über Pointer/Iteratoren. Diese Operatoren werden hauptsächlich für Smartpointer- und Iteratorklassen überladen.
Übliche Deklaration:T* X::operator->() const;
Der Operator liefert einen normalen Pointer oder ein Objekt einer Klasse, die wiederum den
operator->
überladen hat. Das Pointerobjekt selber wird dabei nicht verändert.
Übliche Implementierung:
Deroperator->
liefert den üblicherweise in Pointer/Iteratorobjekten eingebetteten Pointer/Iterator zurück.
Besonderheit: Der Pointerzugriff-Operator kann ausschließlich als Klassenmethode überladen werden. Wird kein normaler Pointer zurückgeliefert, dann hat ein Aufruf des Operators zur Folge dass für das zurückgelieferte Objekt wiederoperator->
aufgerufen wird. Dadurch können z.B. Smartpointerklassen mehrfach verschachtelt werden.3.17 operator->*
Semantik: Memberpointerzugriff über Pointer/Iteratoren. Der Zugriff
objectPtr->*memPtr
hat für normale Pointer die selbe Funktionalität wie(*objectPtr).*memPtr
. Der Operator wird in der Praxis nur selten überladen, da der alternative Zugriff schon durch Überladung des Dereferenzierungsoperators möglich ist.
Mögliche Implementierung:template <typename T, class V> T& X::operator->*(T V::* memptr) { return (operator*()).*memptr; }
X ist dabei der Smartpointer/Iterator-Typ, für den der Operator überladen wird, T ist der Typ des Members, auf den der Memberpointer verweist, und V ist der Typ des Objektes, auf das der Smartpointer verweist (X::value_type) oder eine Basisklasse hiervon. Eine Übergabe anderer Memberpointer wäre zwar auch möglich, allerdings würde der Compiler die Anwendung des
operator.*
dann bemängeln.3.18 operator& (unär)
Semantik: Adressoperator. Liefert die Adresse des Objekts. Da diese Semantik häufig benutzt wird, ist es meist schwer bis unmöglich eine sinnvolle Überladung des Adressoperators zu definieren. Sollte dennoch eine Überladung erfolgen, so sollte sie sorgfältig dokumentiert werden. Außerdem sollte eine separate Möglichkeit für eine Überprüfung der Objektidentität zur Verfügung gestellt werden, da der übliche Vergleich der Adressen zweier Objekte damit nicht mehr möglich ist.
3.19 operator,
Semantik: Der normale Kommaoperator, angewandt auf zwei Ausdrücke. Wertet beide Ausdrücke aus und gibt das Ergebnis des zweiten Ausdrucks zurück. Allgemein wird er nur selten verwendet, meist um mehrere Ausdrücke mit Seiteneffekten an Stellen auszuwerten, an denen nur ein einzelner Ausdruck erlaubt ist. Das Standardbeispiel hierfür ist der
for
-Schleifen Kopf, z.B. wenn bei jedem Schleifendurchlauf mehrere Variablen inkrementiert werden sollen. Es wird allgemein davon abgeraten, diesen Operator zu überladen, zumal die Auswertungsreihenfolge der beiden Argumente unbestimmt ist im Gegensatz zum eingebautenoperator,
, bei dem immer das linke Argument zuerst ausgewertet wird. Allerdings wird er inboost::assign
überladen, um das Erstellen von Listen zur Initialisierung von Containern zu ermöglichen.3.20 operator~
Semantik: Komplementoperator. Einer der am seltensten benutzen Operatoren in C++.
Übliche Deklaration: Keine. Sollte aber wie alle unären Operatoren als Methode seiner Klasse definiert werden.3.21 operator new, new[], delete, delete[]
Semantik: Speicherallokation und -freigabe. Dabei sind
operator new
unddelete
für einzelne Objekte undoperator new[]
unddelete[]
für dynamisch allokierte Arrays zuständig.
Übliche Deklaration:static void* X::operator new(std::size_t n); static void* X::operator new[](std::size_t n); static void* X::operator new(std::size_t n, void* ptr); static void X::operator delete(void* p); static void X::operator delete[](void* p);
Die klassenspezifischen Speicherverwaltungsoperatoren müssen als statische Methoden implementiert werden. Die Operatoren
new
undnew[]
zur Speicherallokation erwarten als erstes Argument einstd::size_t
das angibt, wie viel Speicher (in Bytes) benötigt wird. Zurückgegeben wird ein Pointer auf void, der auf diesen Speicher verweist. Die Operatorendelete
unddelete[]
erwarten als erstes Argument einen Pointer aufvoid
, der auf den freizugebenden Speicher verweist. Alle vier Operatoren dürfen mit erweiterten Argumentlisten definiert werden, die zusätzlichen Argumente müssen dann beim Aufruf mitübergeben werden.
Implementation: Eine umfassende Besprechung der Speicherverwaltungsoperatoren und ihrer Anwendung ist für den zweiten Teil des Artikels geplant, daher werden hier nur ein paar wichtige Punkte umrissen:
Deroperator new
wirft standardmäßig eine Exception vom Typstd::bad_alloc
wenn nicht genug Speicher allokiert werden konnte. Es gibt hauptsächlich aus Kompatibilitätsgründen noch eine weitere Version, die als zweites Argument eine Referenz auf einstd::nothrow
Objekt erwartet. Diese Version vonnew
liefert bei Fehlschlag einen Nullpointer. Eine weitere Überladung vonnew
ist die oben angedeutete Version mit einem Pointer als zweites Argument. Diese Version geht davon aus, dass der nötige Speicher bereits allkoiert wurde und liefert einfach den Pointer wieder zurück. Das neue Objekt wird dann in dem bereits vorher allokierten Speicher konstruiert, der Vorgang wird als "placement new" bezeichnet.
Wie beioperator new
gibt es auch beioperator delete
einenothrow
-Version. Diese wird allerdings nur der Vollständigkeit halber angeboten, dadelete
grundsätzlich keine Exceptions schmeißen sollte, um Exceptionsicherheit zu ermöglichen.
Abgesehen vom placement new gelten die Ausführungen analog für die Array-Versionen der Operatoren.
Besonderheiten: Es gibt globale Versionen der Speicherverwaltungsoperatoren, die standardmäßig für alle Objekte aufgerufen werden, die keine entsprechenden klassenspezifischen Operatoren haben. Diese globalen Operatoren können überschrieben werden, allerdings gibt es dann innerhalb des Programms keine Möglichkeit mehr, auf die vom Compiler bereitgestellten globalen Operatoren zuzugreifen.3.22 Konvertierungsoperatoren
Semantik: Ermöglicht implizite Konvertierungen von Objekten der Klasse in andere Datentypen.
Deklaration:X::operator T() const;
Ein Rückgabetyp wird nicht angegeben, da durch den Namen der Rückgabetyp T bereits vorgegeben ist. Konvertierungsoperatoren müssen Klassenmethoden sein. Als Zieltypen sind alle bereits deklarierten nichtprivaten, nichtabstrakten Typen erlaubt. Das Ursprungsobjekt wird normalerweise nicht verändert.
Übliche Implementation: Es wird ein Objekt des Zieltyps erzeugt, das den Zustand des Ursprungsobjekts oder einen wesentlichen Aspekt dieses Zustands repräsentiert.
Besonderheiten: Wenn an einer Stelle wo ein Objekt vom Typ A erwartet wird, ein Ausdruck vom Typ B vorgefunden wird, prüft der Compiler automatisch auf mögliche implizite Konvertierungen von B nach A. Es ist für einen Entwickler häufig nicht auf den ersten Blick ersichtlich, wo implizite Konvertierungen vorgenommen werden. Konvertierungsoperatoren können zwar oft Schreibarbeit sparen, sorgen aber auch hin du wieder für überraschende Fehler, wenn Konvertierungen vorgenommen werden wo man eigentlich keine erwartet. Außerdem kann das Vorhandensein eines Konvertierungsoperators zu Zweideutigkeiten führen, z.B. wenn es in der Zielklasse einen Konvertierungskonstruktor der FormT::T(X const&)
gibt, bei dem eine implizite Konvertierung nicht durch das Schlüsselwortexplicit
verboten wird. Konvertierungsoperatoren sollten daher spärlich und mit Bedacht angeboten werden.Quellen
Die meisten der oben beschriebenen Sachverhalte gehören im Grunde zum "C++ Allgemeinwissen". Sie entstammen nicht einzelnen Quellen sondern werden in diversen Büchern, Internetseiten und anderen Quellen immer wieder angesprochen.
Beim Schreiben des Artikels habe ich zur Rückversicherung bei technischen Einzelheiten eine C++-Referenz benutzt:
- Dirk Louis: Schnellübersicht C / C++. Die praktische Referenz. Markt & Technik 2003
Außerdem war es etwas knifflig, etwas über das Überladen des
operator->*
herauszufinden. Einzige mir bekannte Quellen hierzu:- http://www.briceg.com/eckel/one/Chapter12.html
- der C++ Standard
Weitere Anlaufstellen für den interessierten Leser sind:
- Scott Meyers: Effective C++
- Scott Meyers: More Effective C++
- Herb Sutter: Exceptional C++
- Herb Sutter: More Exceptional C++
- Herb Sutter: Exceptional C++ Style
- Herb Sutter, Andrei Alexandrescu: C++ Coding Standards
- und selbstverständlich Google, z.B. "C++ operator overloading"
-
Okay, hab mal die Rechtschreibfehler beseitigt. War nicht viel, nur du hast echt ein Kommaproblem Recht oft zu viele, manchmal zu wenige
Von meiner Seite ist der dann fertig soweit und kann auf E
-
GPC schrieb:
nur du hast echt ein Kommaproblem Recht oft zu viele, manchmal zu wenige
Joa... Das Problem der Generation "wir kriegen in der Oberstufe mal eben noch die neue Rechtschreibugn untergejubelt" hier X-Tag gesetzt, das Ganze mit E-Tag gepostet
-
Dieser Thread wurde von Moderator/in GPC aus dem Forum Die Redaktion in das Forum Archiv verschoben.
Im Zweifelsfall bitte auch folgende Hinweise beachten:
C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?Dieses Posting wurde automatisch erzeugt.