Überladung von Operatoren in C++ (Teil 1)



  • Ü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, 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 einem späteren Artikel.

    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 wie

    Matrix 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 auf 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 wie append() , merge() etc. Allerdings gibt es in der Bibliothek boost::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 grundsätzlich nicht statisch mit Ausnahme von operator new und operator delete . 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
    

    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: ’ = ’, ’ -> ’, ’ () ’, ’ [] ’, ’ ->* ’ und 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 ( typedef s auf andere Typen zählen dabei 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 Poitnerzugriffs-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 mit lhs 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 Zuweisung x = t mit verschiedenen Typen X und T meist auch eine implizite Umwandlung von T nach X vorhanden ist, so dass der operator=(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.
    Der operator= fällt unter die "Regel der großen Drei", die 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 keinen operator= 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 den operator= als private 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:

    const X operator+(X const& lhs, X const& rhs);
    

    Übliche Implementierung:

    const 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 des operator+= sinnvoll, um die kürzere Schreibweise a += b an Stelle von a = a + b zu ermöglichen. Um das Verhalten beider Operatoren konsistent zu halten ist es daher üblich, operator+ im Sinne von operator+= zu implementieren wie gezeigt. 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 mittels operator+= 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:

    const X X::plus(X const& rhs) const; //Implementiert die Addition
    const 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 in std::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 eine Referenz auf das Objekt wieder zurückgegeben. Wenn die Operatoren Zugriff auf private Elemente der Klasse X benötigen müssen sie entweder als friend 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 mit a = 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 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.
    Der operator!= wird allgemein durch einfache Negation des operator== 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 auf operator== 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. Analog operator-- 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 entstehen 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: Der operator() 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: Der operator[] kann nur als Klassenmethode definiert werden.

    3.13 operator!

    Semantik: Negation, Verneinung. Der operator! impliziert einen booleschen Kontext, im Gegensatz zum Komplement-Operar operator~ . 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 mit operator! 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 des operator! mit der üblichen Semantik überflüssig macht.
    Übliche Deklaration: bool X::operator!() const;
    Wie alle unären Operatoren sollte der operator! 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 Kurzschlussverhalten nicht 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 namens value_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:
    Der operator-> 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 wieder operator-> 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 auszuwerten an Stellen, 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 eingebauten operator, , bei dem immer das linke Argument zuerst ausgewertet wird. Allerdings wird er in boost::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 und delete für einzelne Objekte und operator new[] und delete[] 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 und new[] zur Speicherallokation erwarten als erstes Argument ein std::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 Operatoren delete und delete[] erwarten als erstes Argument einen Pointer auf void , 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:
    Der operator new wirft standardmäßig eine Exception vom Typ std::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 ein std::nothrow Objekt erwartet. Diese Version von new liefert bei Fehlschlag einen Nullpointer. Eine weitere Überladung von new 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 bei operator new gibt es auch bei operator delete eine nothrow -Version. Diese wird allerdings nur der Vollständigkeit halber angeboten, da delete 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 Form T::T(X const&) gibt, bei dem eine implizite Konvertierung nicht durch das Schlüsselwort explicit 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:

    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"


  • Hey, guter und sinnvoller Artikel! Evtl könntest du noch dazunehmen, dass beim operator[] manchmal Proxy-Objekte zurückgegeben werden, wie beim vector<bool> oder 2D-Arrays, um z.B. table[x][y] zu ermöglichen 🙂



  • Sehr schöner Artikel, pumuckl. Die Thematik ist sehr gut beschrieben, mir sind nur einige Punkte aufgefallen, die aber ziemliche Details sind. Du kannst ja selber entscheiden, ob du sie ändern willst oder nicht. 😉

    pumuckl schrieb:

    • Mindestens ein Operand muss ein nutzerdefinierter Datentyp sein (Klasse oder struct , typedef s auf andere Typen zählen nicht als eigenständiger Typ).

    Hier würde ich nicht unbedingt "Klasse oder struct " schreiben, denn das legt nahe, dass Strukturen keine Klassen sind. Also eher " class oder struct " oder ähnlich. Im Weiteren könntest du vielleicht auch noch enum -Typen erwähnen.

    pumuckl schrieb:

    • Folgende Operatoren dürfen gar nicht überladen werden: ’ ?: ’, ’ :: ’, ’ . ’, ’ .* ’ , ’ ->* ’, typeid , sizeof und die C++-Cast-Operatoren.

    operator->* darf man überladen, du beschreibst ja später, wie. 🙂
    Dafür kannst du ihn in die Liste der Operatoren aufnehmen, die nur als Memberfunktion möglich sind.

    Wenn du willst, kannst du noch kurz was über Boost.Operator sagen (oder vielleicht in einem späteren Teil).



  • Danke für die Hinweise Nexus, habe das korrigiert. Da der Artikel so schon eine gute Länge hat habe ich mich dagegen entschieden, noch mehr Stoff reinzupacken. Wie die Überschrift schon vermuten lässt und wie ich im einleitenden Absatz geschrieben habe, soll es aber nicht bei dem einen Artikel bleiben. Ich habe bereits mit der Arbeit an Teil 2 begonnen, erstes Thema bisher ist gerade boost::operators. Je nach Länge kommen noch ein oder zwei Themen hinzu, auf meiner Liste für Teil 2, 3, ... n stehen zur Zeit boost::operators, das Safe Bool Idiom, operator new/delete und boost::spirit als Beispiel für eine etwas unorthodoxe Operatorüberladung. Außerdem habe ich seit einer Weile den Kern einer kleinen Library in der Schublade, die sich auf expression templates und ähnliches stützt. Ich könnte mir vorstellen, da auch mal was drüber zu schreiben, wenn ich mal dazu komme die Lib fertigzustellen. Für weitere Themen rund um die Operatorüberladung bin ich natürlich immer offen 🙂



  • Vielleicht sollte man erwaehnen das die Uberladung der drei Operatoren (|| && ,) zu undefinierten Verhalten fuehren kann. Und das weil die Kurzschlußauswertung durch einen Funktionsaufruf ersetzt wird. ME C++ erwaehnt das glaube ich, bin mir aber jetzt nicht sicher.



  • SpongeBob11 schrieb:

    Vielleicht sollte man erwaehnen das die Uberladung der drei Operatoren (|| && ,) zu undefinierten Verhalten fuehren kann. Und das weil die Kurzschlußauswertung durch einen Funktionsaufruf ersetzt wird.

    Beispiel bitte. Es führt IMO nichtmal zu unspezifiziertem oder implementationsdefiniertem Verhalten.



  • SpongeBob11 schrieb:

    Vielleicht sollte man erwaehnen das die Uberladung der drei Operatoren (|| && ,) zu undefinierten Verhalten fuehren kann. Und das weil die Kurzschlußauswertung durch einen Funktionsaufruf ersetzt wird. ME C++ erwaehnt das glaube ich, bin mir aber jetzt nicht sicher.

    Klares jein für die beiden Logik-Operatoren, definitives nein für den Kommaoperator. Der Kommaoperator führt nur dann zu undefiniertem Verhalten, wenn eines seiner Argumente zu undefiniertem Verhalten führt oder wenn die Implementation zu undefiniertem Verhalten führt. Im ersten Fall tut das der eingebaute Kommaoperator dann auch, im zweiten Fall ist einfach die Überladung schlecht.
    Für die logischen Operatoren gilt im Prinzip das Gleiche, hinzu kommt, dass oft Pointerdereferenzierungen die Kurzschlusssemantik ausnutzen, z.B.

    X* xPtr;
    /* ... */
    if (xPtr && xPtr->foo())
      /* ... */
    

    wird op&& jetzt überladen kann sowas zu undefiniertem Verhalten führen, wenn das zweite Argument im Kurzschlussfall zu undefiniertem Verhalten führt. Das ist dann aber auch eine Folge des veränderten Verhaltens bei Überladung, das ich allerdings auch beschrieben habe.



  • Erstmal sehr schöner Artikel.

    hab nur einen kleinen Fehler gefunden

    2.1 Wann sollte man Operatoren überladen?
    ....
    ... für die Streams der Standarbibliothek, weitere Beispiele folgen in Kapitel 4.2.

    nur gibt es kein kapitel 4.2. Hast vielleicht Kapitel 3.2 oder 3.4 gemeint? Soll nicht böse gemeint sein ist mir nur aufgefallen.

    So long



  • Kapitel 4.2 wird kommen, allerdings stimmt die Angabe nicht mehr. Geändert auf "in einem späteren Artikel" 😉



  • Was mich interessieren würde, sind Operatoren bei Vererbungsstrukturen. Im Scott Meyers Buch habe ich da leider keine Antwort gefunden (Kapitel 12 oder 13 war das). Hier meine Frage: http://www.c-plusplus.net/forum/viewtopic-var-t-is-232293.html



  • hab dort geantwortet.



  • Ich würde das hier mit const schreiben, also statt

    X operator+(X const& lhs, X const& rhs)
    

    das hier

    const X operator+(X const& lhs, X const& rhs)
    

    Dann kann man nicht "(a+b)=c" schreiben.



  • *Edit
    Sry, habe etwas in den falschen Thread kopiert.



  • Sehr guter Artikel.

    Solltest du gewillt sein, an der ein oder anderen Stelle etwas ausführlicher zu werden, würde ich mich sehr darüber freuen.

    Mir ist allerdings etwas aus deinem Beispiel aufgefallen. Folgender Code dürfte nicht ganz richtig sein, schließlich hast du extra den Konstruktor programmiert, verwendest ihn aber nicht. Bei der Erzeugung von a, b und c solltest du noch Standardwerte nachreichen, ansonsten kommt bei der Addition nur Mumpitz raus.

    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
    


  • nuke1600 schrieb:

    schließlich hast du extra den Konstruktor programmiert, verwendest ihn aber nicht.

    Doch, er wird für die implizite Konvertierung von int nach Rational verwendet.

    nuke1600 schrieb:

    Bei der Erzeugung von a, b und c solltest du noch Standardwerte nachreichen, ansonsten kommt bei der Addition nur Mumpitz raus.

    Der Code würde gar nicht kompilieren, weil kein Defaultkonstruktor vorhanden ist. Aber wie die Objekte erzeugt werden, ist in dem Beispiel nebensächlich...



  • Nexus schrieb:

    wie die Objekte erzeugt werden, ist in dem Beispiel nebensächlich...

    Richtig. Ein weitgehend vollständiges beispiel für so eine Rational-Zahlenklasse findest du im zweiten und dritten Teil des Artikels.



  • Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?



  • Sone schrieb:

    Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?

    Wo hast du das den her?



  • pyhax schrieb:

    Sone schrieb:

    Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?

    Wo hast du das den her?

    Mein Fehler, hab mich im Standard verguckt. Siehe 13.6.24, war schon ne Weile her, hab mich falsch erinnert... 😃



  • Mmh, wäre es möglich, Beispiele für die Überladung von operator-> und operator->* zu bekommen? Insbesonders, da das hier anders als der Artikel zu behaupten scheint, dass typ * operator->() für den Zugriff auf Elemente von typ gedacht ist (mag aber auch einfach nur schlecht formuliert sein).

    ... Kann mir nicht recht vorstellen, wie man das zu verwenden hat.

    Ich glaube, dass ich damit elegant dieses Ding hier vereinfachen könnte:

    typedef struct Nest
    {
      double wert;
      double * koordinaten;
      double * wertigkeiten;
    }
    
    Nest * LeeresNest(const size_t & dimension, const size_t & bewohner)
    {
      Nest * ausgabe = (Nest *)operator new(2 * sizeof(double *) + (1 + dimension + bewohner) * sizeof(double));
      ausgabe->koordinaten  = (double *)(&ausgabe->wertigkeiten + 1); 
      ausgabe->wertigkeiten = ausgabe->koordinaten + dimension;
    
      return ausgabe;
    }
    

    (siehe dieses Thema, falls die Motivation kümmert)

    Weiterhin:
    Wenn ich mir diesen überladenen Operator anschaue:

    vector<T,Allocator>& operator= (const vector<T,Allocator>& x);
    

    dann wird derselbe so benutzt:

    vector a;
    vector b = a;
    

    Mit diesem hier:

    void* operator new (std::size_t size) throw (std::bad_alloc);
    

    sollte die Verwendung also dergestalt aussehen:

    Nest * (Nest *)new 2 * sizeof(double *) + (1 + dimension + bewohner) * sizeof(double);
    

    Funktioniert aber nicht - die Syntax aus dem oberen Beispiel ist erforderlich. Wie kommt das?

    Gleichsam wird aus

    void* operator new (std::size_t size, void* ptr) throw();
    

    in der Anwendung nicht etwa

    new <wert> <zeiger>;
    

    sondern eines von diesen beiden:

    operator new(<wert>, <zeiger>); // keine Ahnung, wie hier der Konstruktor zu wählen wäre
    new (<zeiger>) <Konstruktor>;
    

    Praktisch. Aber woher!?


Anmelden zum Antworten