C++ Tutorial

Allgemeines Überladen von Operatoren

Für Klassen können alle Operatoren überladen werden, mit folgenden Ausnahmen:

  • Punktoperator .
  • Dereferenzierungsoperator für Zeiger auf Methoden .*
  • Gültigkeitsbereichsoperator ::
  • Bedingungsoperator :?
  • sizeof Operator

Um einen Operator zu überladen, gibt es zwei Möglichkeiten:

1.) durch eine nicht-statische Methode oder
2.) durch eine Funktion, welche in der Regel als friend-Funktion definiert ist (friend-Funktionen werden später erläutert).

Binäre Operatoren

Überladen durch nicht-statische Methode

Wird ein binärer Operator (Operator mit zwei Operanden wie z.B. '+' oder '*') überladen, wird die Methode

RTYP CANY::operator OSYMBOL (DTYP val) const;

verwendet. RTYP ist der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators (z.B. '+' für die Addition) für die Klasse CANY. Da die Methode im Kontext des ersten, linken Operanden aufgerufen wird, gibt DTYP den Datentyp des zweiten, rechten Operanden an.

Beispiel: Für eine Klasse Complex soll der Plus-Operator definiert werden, sodass folgende Operation möglich ist:

comp3 = comp1 + comp2;

Da eine Addition von zwei Complex-Objekten wieder ein Complex-Objekt ergibt, muss die Methode ein Complex-Objekt zurückgeben, welches das Ergebnis enthält. Damit besitzt die Methode folgende Deklaration:

1: class Complex
2: {
3:    double real;    // Realanteil
4:    double imag;    // Imaginäranteil
5: public:
6:    ...
7:    Complex operator + (const Complex& op2) const;
8: };

Die Methode ist als const Methode deklariert, da sie die Eigenschaften des linken Operanden, in dessen Kontext sie aufgerufen wird, nicht verändert.

Anschließend ist die Methode zu definieren. Die Addition von zweier Complex-Objekte addiert jeweils die Real- und Imaginäranteile der beiden Objekte.

1: // Überladener Plus-Operator
2: Complex Complex::operator + (const Complex& op2) const
3: {
4:    // temp. Hilfsobjekt mit akt. Objekt initialisieren
5:    // (Aufruf copy-ctor)
6:    Complex temp(*this);
7:    // 2. Operand hinzuaddieren
8:    temp.real += op2.real;
9:    temp.imag += op2.imag;
10:    // temp. Objekt zurückgeben
11:    return temp;
12: }

Zunächst wird ein lokales Objekt der Klasse Complex definiert, welches bei seiner Definition mit dem aktuellen Objekt, dies ist der linke Operand des + Operators, initialisiert wird (Aufruf des Kopierkonstruktors). Anschließend wird zu diesem lokalen Objekt das zweite Objekt, der rechte Operand des + Operators, hinzuaddiert. Es muss hier unbedingt ein temporäres Hilfsobjekt zu Hilfe genommen werden, denn eine Addition verändert nicht den Inhalt der Operanden!

Sehen wir uns ein weiteres Beispiel für das Überladen des Plus-Operators der Klasse Complex an. Anstelle zwei Complex-Objekte zu addieren, soll jetzt zu einem Complex-Objekt ein double-Wert addiert werden, d.h., folgende Operation soll erlaubt sein:

comp2 = comp1 + 1.2;

Der Returntyp der Methode ändert sich nicht, da auch hier das Ergebnis der Addition ein Complex-Objekt ist. Lediglich der Datentyp des Parameters der Methode ändert sich, da er den Datentyp des rechten Operanden widerspiegelt. In unserem Fall ist der Datentyp ein double. Im nachfolgenden Beispiel wird dieser double-Wert zum Realanteil der komplexen Zahl addiert.

1: class Complex
class="text-primary">2: {
3:    ...
4: };
5: // Überladener Plus-Operator
6: Complex Complex::operator + (double val) const
7: {
8:    // temp. Hilfsobjekt definieren und mit
9:    // akt. Objekt initialisieren
10:   Complex temp(*this);
11:   // Operand hinzuaddieren
12:   temp.real += val;
13:   // temp. Objekt zurückgeben
14:   return temp;
15: }

Die umgekehrte Operation

comp2 = 1.2 + comp1;

kann nicht mit einer Methode realisiert werden! Wie erwähnt, wird die Operator-Methode im Kontext des linken Operanden aufgerufen, und dies wäre im Kontext von double. Da für die intrinsischen Datentypen keine Operatoren überladen werden können, muss für diese Operation, wie gleich beschrieben, eine friend-Funktion verwendet werden.

Überladen durch Funktionen

Wie am Anfang des Kapitels erwähnt, wird hierfür eine friend-Funktion benötigt. Die Besonderheit einer friend-Funktion liegt darin, dass sie Zugriff auf alle Member einer Klasse hat, auch auf deren private-Member. Mehr zu friend-Funktionen später.

Wird ein binärer Operator durch eine Funktion überladen, wird folgende Funktion hierfür eingesetzt:

RTYP operator OSYMBOL (DTYP1 op1, DTYP2 op2);

RTYP ist wieder der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators. Da die Funktion nicht mehr im Kontext eines Objekts aufgerufen wird, und damit implizit der Datentyp des linken Operanden definiert ist, benötigt sie zwei Parameter. Der erste Parameter entspricht dem linken Operanden und der zweite Parameter dem rechten Operanden.

Sehen wir uns an wie der Plus-Operator für die Klasse Complex durch eine Funktion überladen wird, um zu einem Complex-Objekt einen double-Wert zu addieren. Beachten Sie bitte die Deklaration der friend-Funktion innerhalb der Klasse Complex. Die friend-Deklaration gleich bis auf das vorangestellte Schlüsselwort friend der Deklaration einer Member-Operatorfunktion.

1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5:    ...
6:    // Funktion als friend-Funktion deklarieren
7:    friend Complex operator + (const Complex& op1,
8:                               double val);
9: };
10: // Überladener Plus-Operator
11: Complex operator+ (const Complex& op1, double val)
12: {
13:    // temp. Hilfsobjekt
14:    Complex temp(op1);
15:    // 2. Operand hinzuaddieren
16:    temp.real += val;
17:    // temp. Objekt zurückgeben
18:    return temp;
19: }

Wurde der Plus-Operator wie oben angegeben überladen, kann damit nur die Operation

comp2 = comp1 + 1.2;

ausgeführt werden. Soll die Operation

comp2 = 1.2 + comp1;

ausgeführt werden, ist eine weitere friend-Funktion zu definieren, bei der die beiden Parameter vertauscht sind, d.h.

Complex operator+ (double val, const Complex& op2);

Ein weiterer Anwendungsfall für überladene Operatoren sind enum-Klassen. Wie im Kapitel über den enum-Datentyp erwähnt, können mit Enumeratoren aus enum-Klassen standardmäßig keine Operationen durchgeführt werden. Sollen zum Beispiel Enumeratoren verodert werden, ist dafür eine Funktion zu definieren. Als Parameter erhält die Funktion wiederum den rechten und den linken Operanden, welche jetzt vom Typ der enum-Klasse sind. Innerhalb der Funktion werden die Enumeratoren auf den zugrunde liegenden Datentyp konvertiert, dann verodert und zum Schluss ein neues enum-Objekt mit dem Ergebnis der Veroderung erzeugt und zurückgegeben.

1: #include <utility>
2: // Enum-Klasse definieren
3: enum class Color {RED, GREEN, BLUE};
4:
5: // Oder-Operator fuer Enum-Klasse definieren
6: constexpr Color operator | (Color col1, Color col2)
7: {
8:    return Color ((std::to_underlying(col1) |
9:                   std::to_underlying(col2));
10: }
11:
12: int main()
13: {
14:    // Farben verodern, 'ruft' den Operator | fuer
15:    // die enum-Klasse auf.
16:    Color newCol = Color::GREEN | Color::BLUE;
17:    ...
18: }

Beachten Sie im obigen Beispiel bitte, dass die Funktion als constexpr Funktion implementiert ist. Dies erlaubt den Compiler beim Übersetzen des Programms das Ergebnis der Funktion zu berechnen und das Ergebnis direkt in den Code einzusetzen.

Unäre Operatoren

Überladen durch Methoden

Um einen unären Operator, wie z.B. den NOT-Operator !, durch eine nicht-statische Methode zu überladen, wird folgende Methode verwendet:

RTYP CANY::operator OSYMBOL () const;

RTYP ist der Returntyp des Operators und OSYMBOL wiederum das Symbol des zu überladenden Operators der Klasse CANY. Da unäre Operatoren nur einen Operanden besitzen, benötigt die Methode keine Parameter.

Beispiel: Für die Klasse Complex soll der Vorzeichenoperator überladen werden, sodass folgende Operation definiert ist:

comp2 = -comp1;

Beachten Sie bitte, dass es sich hier um den Vorzeichenoperator und nicht um den Minus-Operator handelt! Beide besitzen zwar das gleiche Symbol, der Minus-Operator benötigt zwei Operanden und der Vorzeichenoperator nur einen. Da das Ergebnis des Vorzeichenoperators immer vom Typ des Operanden ist, liefert die Operator-Methode ebenfalls ein Complex-Objekt zurück. Im Beispiel kehrt der Vorzeichenoperator die Vorzeichen des Real- und Imaginäranteils um. Des Weiteren ist beim Überladen von Operatoren besonders darauf zu achten, wann die Operanden verändert werden dürfen und wann nicht!

1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5: public:
6:    ...
7:    Complex operator -() const;
8: };
9: // Überladener Vorzeichenoperator
10: Complex Complex::operator- () const
11: {
12:    Complex temp;
13:    temp.real = -real;
14:    temp.imag = -imag;
15:    return temp;
16: }

Überladen durch Funktionen

Um einen unären Operator durch eine Funktion zu überladen, ist folgende Funktion zu verwenden:

RTYP operator OSYMBOL (CAny& op);

RTYP ist wieder der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators. Da die Funktion nicht mehr im Kontext eines Objekts aufgerufen wird, muss sie eine friend-Funktion der Klasse sein und benötigt einen Parameter. Dieser Parameter gibt den Operanden an und ist immer eine Referenz vom Typ der Klasse, für die der Operator überladen wird.

Überladen wir den NOT-Operator! für die Klasse Complex durch eine Funktion, um folgende Abfrage zu ermöglichen:

if (!comp1)
   ...

Der NOT-Operator muss laut C++-Standard einen bool-Wert zurückliefern, denn entweder ist die Bedingung erfüllt oder nicht. Als Parameter erhält die Funktion eine const-Referenz auf den Operanden. Im Beispiel liefert der NOT-Operator dann true zurück, wenn sowohl der Real- wie auch der Imaginäranteil 0.0 ist.

1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5:    ...
6:    // Funktion als friend-Funktion deklarieren
7:    friend bool operator ! (const Complex& op);
8: };
9: // Überladener NOT-Operator
10: bool operator! (const Complex& op)
11: {
12:    return ((op.real == 0.0) && (op.imag == 0.0));
13: }

Vergleichsoperatoren (spaceship operator)

Da der C++-Standard nicht definiert, wie Objekte zu vergleichen sind, sind hierfür entsprechende Operatoren zu überladen.

Vor C++20 musste für jeden Vergleichsoperator <, <=, ==, !=, >= und > eine eigene Methode/Funktion definiert werden. Dies führte häufig dazu, dass eine Unmenge von fast identischem Code erforderlich wurde, um Objekte miteinander zu vergleichen. Ab C++20 muss prinzipiell nur noch der Operator <=>, spaceship-Operator oder 3-Wege-Vergleich genannt, definiert werden.

Die Syntax für die Anwendung des spaceship-Operators ist identisch mit der der 'normalen' Vergleichsoperatoren.

auto result = op1 <=> p2;

Im Gegensatz zu den 'normalen' Vergleichsoperatoren liefert der spaceship-Operator als Ergebnis nicht true oder false zurück, sondern -1 wenn op1< op2, 0 wenn op1==op2 und 1 wenn op1>op2. Der Datentyp des Ergebnisses ist entweder vom Typ strong_ordering, weak_ordering oder partial_ordering und hängt von den Datentypen der Operanden ab. Wann welcher Datentyp zurückgeliefert wird, ist auf https:://www.cppreference.com unter dem Stichwort Default comparisons zu finden. In der Regel wird der Datentyp des Ergebnisses automatisch bestimmt, indem als Datentyp auto angegeben wird.

Damit der Compiler mithilfe des spaceship-Operators die notwendigen Methoden/Funktionen für die Vergleiche erstellen kann, ist die Header-Datei <compare> einzubinden.

Überladen durch Methode

Die einfachste Methode, den spaceship-Operator für eine Klasse zu definieren, ist die Definition der Methode

auto operator <=> (const CAny& rhs) const = default;

Durch die Angabe von = default generierte der Compiler alle notwendigen Vergleichsfunktionen. Die generierten Vergleichsfunktionen vergleichen alle Eigenschaften der Objekte in der Reihenfolge, in der sie in der Klasse definiert sind.

Sehen wir uns dies am Beispiel der Klasse Complex an, die um eine zusätzliche Eigenschaft id erweitert wurde.

1: #include <print>
2: #include <compare>
3:
4: class Complex
5: {
6:    double real;
7:    double imag;
8:    int id;
9: public:
10:   Complex(double r, double i, int x): real(r),imag(i),id(x)
11:   {}
12:   auto operator <=> (const Complex& rhs) const = default;
13: };
14:
15: int main()
16: {
17:    Complex comp1{1.1, 1.8, 1};
18:    Complex comp2{1.1, 1.8, 2};
19:    std::println("c1==c2: {}", comp1 == comp2);
20:    std::println("c1<c2: {}", comp1 < comp2);
21: }

c1==c2: false
c1<c2: true

Die Real- und Imaginärteile der Objekte sind identisch, jedoch ist die Eigenschaft id des Objekts comp1 kleiner als die des Objekts comp2. Sollen nur die Real- und Imaginärteile verglichen werden, ist der spaceship-Operator explizit zu definieren. Da der spaceship-Operator keinen bool-Wert zurückliefert, sind die zu vergleichenden Eigenschaften ebenfalls mit dem spaceship-Operator zu vergleichen. Soll zusätzlich ein Vergleich auf Gleichheit durchgeführt werden, ist zusätzlich der == Operator zu definieren.

1: #include <print>
2: #include <compare>
3:
4: class Complex
5: {
6:    double real;
7:    double imag;
8:    int id;
9: public:
10:   Complex(double r, double i, int x): real(r),imag(i),id(x)
11:   {}
12:   // Expliziter spaceship-Operator
13:   // Nur Vergleich der Real- und Imaginaeranteile
14:   auto operator <=> (const Complex& rhs) const
15:   {
16:      // Real-Anteile vergleichen,
17:      // wenn ungleich, Vergleichsergebenis zurueckgeben
18:      if (auto cmp = real<=>rhs.real; cmp != 0)
19:         return cmp;
20:      // ansonsten Vergleichsergebnis des Vergleichs
21:      // der Imaginaeranteile zurueckgeben
22:         return imag <=> rhs.imag;
23:   }
24:   bool operator == (const Complex& rhs) const
25:   {
26:      return (real==rhs.real) && (imag==rhs.imag);
27:   }
28: };
29:
30: int main()
31: {
32:    Complex comp1{ 1.1, 1.8, 1 };
33:    Complex comp2{ 1.1, 1.8, 2 };
34:    std::println("c1==c2: {}", comp1 == comp2);
35:    std::println("c1<c2: {}", comp1 < comp2);
36: }

c1==c2: true
c1<c2: false

Überladen durch Funktion

Soll der spaceship-Operator durch eine Funktion überladen werden, ist analog zum Überladen von binären Operatoren vorzugehen.

1: class Complex
2: {
3:    double real;
4:    double imag;
5:    int id;
6: public:
7:    Complex(double r, double i, int x): real(r),imag(i),id(x)
8:    {}
9:    friend auto operator <=> (const Complex& lhs,
10:                             const Complex& rhs);
11:   friend bool operator == (const Complex& lhs,
12:                            const Complex& rhs);
13: };
14:
15: auto operator <=> (const Complex& lhs, const Complex& rhs)
16: {
17:    if (auto cmp = lhs.real <=> rhs.real; cmp != 0)
18:       return cmp;
19:    return lhs.imag <=> rhs.imag;
20: }
21:
22: bool operator == (const Complex& lhs, const Complex& rhs)
23: {
24:    return lhs.real == rhs.real && lhs.imag == rhs.imag;
25: }

const-Objekte und überladene Operatoren

Bekanntermaßen ist die Syntax für das Überladen von Operatoren durch eine Methode

RTYP CANY::operator OSYMBOL (DTYP val) const;

Und wie erwähnt, erfolgt der Aufruf des Operators im Kontext des linken Operanden und der rechte Operand wird als Parameter an die Methode übergeben.

Angenommen, es ist für eine Klasse CAny der Plus-Operator zu überladen, um die nachfolgenden Operationen durchführen zu können:

1: CAny ncObject1, res;
2: const CAny cObject2;
3: res = ncObject1 + ncObject1;    // non_const+non_const
4: res = ncObject1 + cObject2;     // non_const+const
5: res = cObject2 + ncObject1;     // const+non_const
6: res = cObject2 + cObject2;      // const+const

Welche Operator-Methoden müssten definiert werden, damit diese Operationen durchgeführten werden können? Nachfolgend sind die Operator-Methoden in der Reihenfolge aufgeführt, wie sie für die Operationen benötigt werden.

1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5: public:
6:    ...
7:    Complex operator + (Complex& op2);
8:    Complex operator + (const Complex& op2);
9:    Complex operator + (Complex& op2) const;
10:   Complex operator + (const Complex& op2) const;
11: };

Ist der rechte Operand ein const-Objekt, muss der Parameter der Operator-Methode ebenfalls ein const sein. Ist dagegen der linke Operand ein const, muss die Operator-Methode selbst const sein, da ein const-Objekt laut Definition nicht verändert werden kann.

Aber keine Panik! Es sind nicht alle vier Operator-Methoden für überladene Operatoren zu definieren. Da ein nicht-const-Objekt stets in ein const-Objekt konvertiert werden kann (aber nicht umgekehrt), reicht die letzte Methode aus.

Sonstige Hinweise zum Überladen von Operatoren

Zum Schluss dieses Kapitels vier Hinweise:

  • Das Überladen eines Operators ändert niemals die Rangfolge der Operatoren, d.h., es gilt weiterhin, dass eine Multiplikation immer vor einer Addition ausgeführt wird.
  • Die Anzahl der Operanden eines Operators ist fest vorgegeben. So benötigt der Plus-Operator immer zwei Operanden.
  • Sind z.B. die Operatoren für = und * überladen, ist nicht damit nicht automatisch der Operator *= überladen.
  • Und sollten Sie Zweifel haben, wann ein überladener Operator eine Referenz zurückliefert, und wann ein Objekt, können Sie sich an folgende Daumenregel orientieren: Ein überladener Operator liefert in der Regel dann eine Referenz zurück, wenn der Operand verändert wird. In allen anderen Fällen ist ein Objekt zurückzuliefern.

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz