[X] Ü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 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 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 (Klasse oder struct , 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 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:

    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 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:

    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 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"


  • Es ist schön, dass sich hier mal wieder was tut. 🙂 👍

    Ich hab mir den Artikel jetzt nicht ganz durchgelesen, allerdings finde ich einige Teile sehr schwer zu lesen weil IMHO die Formatierung nicht hilft eine Idee rüber zu bringen. Zum Beispiel:

    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, solang mindestens eines der Argumente ein nutzerdefinierter Datentyp ist.

    Das sind fast 1000 Zeichen und kein einziger Zeilenumbruch. Eventuell ist es auch sinnvoll 2 zu verwenden. Dadurch ist der Text durch eine Leerzeile getrennt und dadurch wird es deutlicher, dass hier eine neue Idee beginnt. Ist halt eine Einschränkung des Boards mit dem wir leben müssen. :xmas1:

    Manchmal ist weniger auch mehr. Du unterscheidest hier zwischen Zuweisungsoperatoren und arithmetischen Operatoren. Ist das bereits in der Einleitung nötig? Würde es nicht reichen einfach von Operatoren zu reden?



  • Um ehrlich zu sein halte ich persönlich nicht allzuviel davon, Text durch Leerzeilen künstlich zu zerstückeln. Der Teil den du zitiert hast ist auch nicht so kompliziert, dass man ihn großartig auflockern müsste, letzlich handelt es sich lediglich um eine kurze Einführung ins Thema. Ist denke ich Geschmackssache. Was sagen die anderen dazu?

    Was deinen zweiten Punkt angeht: Ich habe lediglich zwei Beispiele für häufig vorkommende Operatoren gegeben und dem Kind gleich einen Namen gegeben. Allerdings halte ich die Unterscheidung für sinnvoll, da das Beispiel der fehlenden Operatorüberladung bei Zuweisungen weniger offensichtlich ist als bei anderen Operatoren.



  • Änderungsnotiz:

    Absatz 3.11, operator[]. Habe noch was zur const-Überladung hinzugefügt.



  • Hi,

    also mir gefällt der Artikel sehr gut! 🙂 Cool finde ich v.a. die detaillierte Unterteilung der Beschreibung eines operators in Semantik, Implementierung usw.

    Allerdings hat Ben04 nicht ganz unrecht, wenn er an einigen "strategisch wichtigen" Stellen im Text mehr Leerzeilen möchte, um die Übersichtlichkeit zu erhöhen. Ich denke ein paar schaden nicht und fördern die Leserlichkeit enorm.

    Ansonsten sind mir folgende Dinge aufgefallen:

    • Abschnitt 2.2: Was hältst du davon, wenn du noch den generellen Leitsatz "Operatoren nur als Klassenmember implementieren wenn nötig, sonst freie Funktionen bevorzugen" einbaust?
    • Abschnitt 3.1: Die Prüfung auf Selbstzuweisung ist idR unnötig und erfolgt sicher nicht aus Performancegründen :D. Bei auf dem Stack allokierten Membervariablen ist es eh wurscht und wenn man Pointer bzw. andere Ressourcen hat (auf die Problematik bist du ja eingegangen) kann man die Selbstzuweisung leicht umgehen indem man erst eine Kopie der Ressource allokiert und dann die alte löscht. Dieses Verfahren ist der Selbstzuweisung imo vorzuziehen.
    • Abschnitt 3.13: Da ist die Übeschrift kaputt
    • Abschnitt 3.20: Du gehst da auf die Konvertierungsproblematik ein, wie wäre es, wenn du noch irgendwo das Schlüsselwort explicit erwähnst?

    Sind eher Kleinigkeiten, sonst ist alles in Ordnung! 🙂

    Cheers

    GPC



  • Änderungsnotiz:

    - Leerzeilen eingefügt (ich beuge mich... ;o))
    - Abschnitt 2.2, Absatz mit allgemeiner Richtlinie bezüglich Klassenmethode vs. freie Funktion
    - Abschnitt 3.1 Absatz bezgl. Test auf Selbstzuweisung umgeschrieben und nach hinten verschoben.
    - Abschnitt 3.13 Überschrift gefixt.
    - Abschnitt 3.20 Satz zum impliziten Konvertierungskonstruktor abgeändert

    PS: frohes neues! 🙂



  • Ich finde es gut, dass du nicht nur auf das eingehst was möglich ist, sondern auch auf das was sinnvoll ist. 🙂 👍 Der Textfluss ist nun auch wesentlich angenehmer.

    GPC schrieb:

    Abschnitt 2.2: Was hältst du davon, wenn du noch den generellen Leitsatz "Operatoren nur als Klassenmember implementieren wenn nötig, sonst freie Funktionen bevorzugen" einbaust?

    Den += würde ich trotzdem als Member implementieren obwohl es auch frei ginge.

    Wie von GPC bereits gesagt: Beim Copy&Swap-Idiom braucht man nicht auf selbstzuweisung zu testen. Dies ist weder im op= noch in der swap Methode notwendig da swap hier immer mit 2 verschiedenen Operanten aufgerufen wird.

    Bei 3.10 operator() würde ich ein kleines Beispiel angeben. Die leeren Klammern bei operator()(Foo, Bar) können einen Stolperstein sein. Ich hatte anfangs jedenfalls ein paar Probleme mit denen.

    Beim Beispiel in 3.4 operator<<, >> fehlt das return. Eventuell noch ein Beispiel der Art

    ostream&operator<<(ostream&out, const Point&p){
        return out<<'('<<p.x<<", "<<p.y<<')';
    }
    

    einbauen, um zu zeigen wie man das oft kurz und elegant implementieren kann.

    Ich weiß nicht ob es sinnvoll ist das an der Stelle zu erwähnen, allerdings kann man die stream-Operatoren auch auf den char-Typ des Streams per template parametrisieren. Damit funktioniert das ganze dann auch mit wstreams (und allen weiteren Streams, sollte es denn irgendwann mal welche geben...)

    Ist es gewollt, dass du beim op@= nicht erwähnst, dass der auch frei sein kann?

    Beim op!= würde ich noch eine Beispielimplentierung mittels op== angeben. Das selbe Spiel dann nochmal für den op<. Den op== kann man auch mittels op< implementieren.

    Beim op++ gibt man in der Regel ein const Foo bzw const Foo& zurück damit Sachen wie

    Foo a;
    ++++a++++;
    

    nicht compilieren.

    ++(*this);

    Die Klammern sind hier nicht notwedig.

    Der Typ des Parameters ist bei der Deklaration frei wählbar, ebenso der Rückgabetyp.

    Die Behauptung stimmt natürlich. Allerdings verstehe ich nicht warum du das hier erwähnst. Das war doch bereits der Fall für alle vorhergehende Operatoren. 😕

    Beim Adressoperator könnte man auf boost::addressof als mahnendes Beispiel verweisen, um zu zeigen was für Tricks nötig werden um an den Zeiger zu kommen wenn der op& überladen wird.

    Beim op new und op delete kenne ich mich nicht wirjlich aus. Des GCC frisst jedenfalls auch Deklarationen ohne static da er dies implizit annimmt. Ich würde diese Operatoren entweder ganz in diesem Teil oder ganz im nächsten behandeln. Da dieser Artikel bereits eine gewisse Länge hat ist es wohl sinnvoller es in den nächsten zu packen.

    Bei ein paar Operatoren hast du ein const angegeben welches eigentlich gar nicht nötig ist. So beispielsweise bei der Dereferenzierung.

    Es fehlen die überladbaren Operatoren <<=, >>=, &=, |=, ^= und ->*.

    Irgendwo sollte IMO noch erwähnt werden welche Operatoren welche Priorität haben und was für Operatoren links- und welche rechtsbündig sind. Zum Beispiel unterscheiden sich << und <<= recht stark diesbezüglich:

    #include <iostream>
    using namespace std;
    
    class Foo{
    public:
    
    	Foo& operator<< (const char* r){
    		cout<<"Foo<<"<<r<<endl;
    		return *this;
    	}
    
    	friend Foo& operator<<= (const char* l, Foo&foo){
    		cout<<l<<"<<=Foo"<<endl;
    		return foo;
    	}
    
    };
    
    int main(){
    	Foo a;
    	a<<"1"<<"2"<<"3";
    
    	"3"<<="2"<<="1"<<=a;
    }
    

    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.

    Eine verrückte Idee für den zweiten Teil: inline-Brainfuck. Ich weiß nicht wie gut das überhaupt geht, es wäre aber jedenfalls ein witziges Beispiel und würde zeigen was man mit Operatoren alles anstellen kann. Es würde glaube ich auch jedem zeigen, dass man es nicht übertreiben sollte. Ohne Expressiontemplates kommt man da aber glaub ich nicht aus. So in etwa könnte das dann aussehen:

    (_<_+_-_>_[_-_,_._],_._,_).exec()
    

    <edit>der . steht leider nicht zur Verfügung. Auch nicht mit exotischen Tricks.</edit>
    wobei die _ halt notwendig sind, damit ein C++ Compiler das schluckt. Wie bereits gesagt, ist eine verrückte Idee und könnte vom Schwierigkeitsgrad auch ein bisschen hoch sein für den Durschnittsleser des ersten Teils.

    PS: Auch frohes neues von mir 🙂



  • Änderunsnotiz:

    - op@= als Klassenmethode zu implementieren in die Richtlinie eingebaut
    - Deklarationsbeispiel für op()
    - return in Beispiel 3.4
    - implementierungsbeispiel op!= eingebaut
    - Postinkrement: Rückgabe konstant.

    Rest folgt noch...

    Einige Kommentare noch:

    Ben04 schrieb:

    Den += würde ich trotzdem als Member implementieren obwohl es auch frei ginge.

    Das sollte auf jeden Fall gemacht werden, da ein freies += eine Konvertierung des ersten Arguments erlaubt und dann nur das temporäre konvertierte Objekt modifiziert würde. Etwas sinnfrei 😉

    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.

    ++(*this);

    Die Klammern sind hier nicht notwedig.

    Erhöhen aber imho die Lesbarkeit und schaden nicht.

    Der Typ des Parameters ist bei der Deklaration frei wählbar, ebenso der Rückgabetyp.

    Die Behauptung stimmt natürlich. Allerdings verstehe ich nicht warum du das hier erwähnst. Das war doch bereits der Fall für alle vorhergehende Operatoren. 😕

    Stimmt natürlich, hab ich ja mit dem Beispiel Kreis+Dreieck->Pyramide drauf hingewiesen. Allerdings beziehen sich die meisten anderen Operatoren üblicherweise auf die Klasse für die sie definiert werden, was beim op[] und () nicht der Fall ist.

    Bei ein paar Operatoren hast du ein const angegeben welches eigentlich gar nicht nötig ist. So beispielsweise bei der Dereferenzierung.

    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...

    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??

    Eine verrückte Idee für den zweiten Teil: inline-Brainfuck. Ich weiß nicht wie gut das überhaupt geht, es wäre aber jedenfalls ein witziges Beispiel und würde zeigen was man mit Operatoren alles anstellen kann. Es würde glaube ich auch jedem zeigen, dass man es nicht übertreiben sollte. Ohne Expressiontemplates kommt man da aber glaub ich nicht aus. So in etwa könnte das dann aussehen:

    (_<_+_-_>_[_-_,_._],_._,_).exec()
    

    <edit>der . steht leider nicht zur Verfügung. Auch nicht mit exotischen Tricks.</edit>
    wobei die _ halt notwendig sind, damit ein C++ Compiler das schluckt. Wie bereits gesagt, ist eine verrückte Idee und könnte vom Schwierigkeitsgrad auch ein bisschen hoch sein für den Durschnittsleser des ersten Teils.

    Mit brainfuck & Co kenn ich mich so garnicht aus. Lediglich der Name ist mir mal untergekommen. Deshalb muss ich in dem Fall leider passen. Mir schwebt allerdings ein dritter Teil zu expression templates vor: Ich hab mal angefangen, etwas zu schreiben was ich als "on demand evaluated expressions" bezeichnet hab: Ein Ausdruck wird zu einem frühen Zeitpunkt zusammengebaut und an Variablen gebunden und zu beliebigen späteren Zeitpunkten mit den aktuellen Werten der Variablen ausgewertet. z.B.:

    int a = 1, b = 2;
    double c = 5.0;
    bool d = false;
    double e = 8.5
    
    ODExpression<bool> ex = ((odExpr(a) * b + c) >= constExpr(e)) && d;
    
    if (ex()) //false, da d == false
      cout << "true" << endl; 
    else 
      cout << "false" << endl;
    
    d = true;
    a = 3;
    c = 2.51;
    
    if (ex()) //true, da a*b=6, c=2.51, d=ture
      cout << "true" << endl; 
    else 
      cout << "false" << endl;
    

    Aber vor dem dritten steht erstmal der zweite Teil 😉



  • 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 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 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 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 mit Ausnahme von operator new und operator 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 oder struct , 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 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". 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 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:

    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 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 (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 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:

    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 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 wieder eine Referenz auf das Objekt 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 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.
    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 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: 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 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 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"


  • 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.


Anmelden zum Antworten