C++ Kurs

Allgemeines Überladen von Operatoren

Die Themen:

Genauso wie im vorherigen Kapitel der Zuweisungsoperator '=' überladen wurde, können für Objekte fast alle anderen Operatoren ebenfalls überladen werden, mit folgenden Ausnahmen:

Ebenfalls nicht überladbar ist das Präprozessor-Symbol '#', aber das versteht sich ja fast von selbst, da die Präprozessoranweisung vor dem Compilerlauf durch den Präprozessor schon ersetzt wird.

Um einen Operator zu überladen stehen prinzipiell zwei Vorgehensweisen zur Verfügung:

Überladen von binären Operatoren

Überladen durch nicht-statische Memberfunktion

Wird ein binärer Operator (das sind Operatoren mit zwei Operanden wie z.B. '+' oder '*') überladen, so wird in der Regel folgende Memberfunktion hierfür eingesetzt:

RVAL CANY::operator OSYMBOL (DTYP);

RVAL 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 Memberfunktion im Kontext des ersten, linken Operanden aufgerufen wird, gibt DTYP den Datentyp des zweiten, rechten Operanden an.

Dazu ein Beispiel. Für die Klasse Complex soll der Plus-Operator definiert werden, so dass folgende Operation möglich ist:

comp3 = comp1 + comp2;

Als erstes ist der Returntyp des überladenen Operators zu bestimmen. Da eine Addition von zwei Complex-Objekten ebenfalls wieder ein Complex-Objekt ergibt, muss die Memberfunktion ein Complex Objekt zurückgeben, welches das Ergebnis enthält. Damit besitzt die Memberfunktion folgende Deklaration:

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex operator + (const Complex& op2) const;
};

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

Anschließend kann die Memberfunktion definiert werden. Eine Addition von zwei Complex-Objekten addiert getrennt die Real- und Imaginäranteile der beiden Objekte.

PgmHeader// Überladener Plus-Operator
Complex Complex::operator + (const Complex& op2) const;
{
    // temp. Hilfsobjekt mit akt. Objekt initialisieren (Aufruf copy-ctor)
    Complex temp(*this);
    // 2. Operanden hinzuaddieren
    temp.real += op2.real;
    temp.imag += op2.imag;
    // temp. Objekt zurückgeben
    return temp;
}

Zuerst 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 rechter Operand des + Operators, hinzuaddiert. Es muss hier unbedingt ein temporäres Hilfsobjekt zu Hilfe genommen werden, denn eine Addition von zwei Objekten darf niemals deren Inhalt verändern!

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

comp2 = comp1 + 1.2;

Am Returntyp der Memberfunktion ändert sich gegenüber vorher nichts, da auch hier das Ergebnis der Addition wiederum ein Complex-Objekt ist. Lediglich der Datentyp des Parameters der Memberfunktion ändert sich, da er den Datentyp des zweiten, rechten Operanden widerspiegelt. In unserem Fall ist der Datentyp also ein double. Im nachfolgenden Beispiel wird dieser double-Wert zum Realanteil der komplexen Zahl addiert.

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex operator+ (double val) const;
};
// Überladener Plus-Operator
Complex Complex::operator + (double val) const
{
    // temp. Hilfsobjekt definieren und mit akt. Objekt initialisieren
    Complex temp(*this);
    // Operand hinzuaddieren
    temp.real += val;
    // temp. Objekt zurückgeben
    return temp;
}

HinweisDie umgekehrte Operation

comp2 = 1.2 + comp1;

können Sie nicht mit einer Memberfunktion realisieren! Wie bereits erwähnt, wird die Operator-Memberfunktion im Kontext des linken Operanden aufgerufen, und dies ist hier im Kontext von double. Da für die Standard-Datentypen keine Operatoren überladen werden können, muss für diese Operation, wie gleich beschrieben, eine normale (friend-)Funktion verwendet werden.

Überladen durch Funktionen

Betrachten wir nun den Fall, dass der Operator durch eine normale Funktion überladen wird und nicht durch eine Memberfunktion. Wie bereits 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, also auch auf die private-Member, besitzt. Mehr zu friend-Funktionen später noch.

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

RVAL operator OSYMBOL (DTYP1 OP1, DTYP2 OP2);

RVAL ist wieder der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators (z.B. '+' für die Addition). Da die Funktion nun nicht mehr im Kontext eines Objekts aufgerufen wird, und damit implizit der Datentyp des linken Operanden definiert ist, benötigt sie auch zwei Parameter. Der erste Parameter gibt hierbei den linken Operanden und der zweite Parameter den rechten Operanden an.

Sehen wir uns an, wie der Plus-Operator für die Klasse Complex durch eine normale 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 der Operatorfunktion.

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
    ....
    // Funktion als friend-Funktion deklarieren
    friend Complex operator + (const Complex& op1, double val);
};
// Überladener Plus-Operator
Complex operator+ (const Complex& op1, double val)
{
    // temp. Hilfsobjekt
    Complex temp(op1);
    // 2. Operanden hinzuaddieren
    temp.real += val;
    // temp. Objekt zurückgeben
    return temp;
}

Die Funktion muss als Returnwert wieder ein Complex-Objekt mit dem Ergebnis der Addition liefern. Im ersten Parameter erhält die Funktion eine Referenz auf den linken Operanden des Operators, in unserem Fall also eine Referenz auf ein Complex-Objekt. Und da der zweite Parameter den rechten Operanden repräsentiert, muss hier ein double-Parameter stehen. Der Ablauf der Funktion selbst entspricht dem des vorherigen Beispiels.

Aber Achtung!

AchtungHaben Sie den Plus-Operator wie oben angegeben überladen, so können Sie damit nur Operationen des folgenden Typs durchführen:

comp2 = comp1 + 1.2;

Wollen Sie auch Operationen vom Typ

comp2 = 1.2 + comp1;

durchführen, so müssen Sie eine weitere friend-Funktion definieren, bei der die beiden Parameter vertauscht sind, d.h.

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

Sehen wir uns noch ein weiteres Beispiel für das Überladen von Operatoren mit Hilfe von Funktionen an. Wie bereits im Kapitel über den enum Datentyp erwähnt, können mit Bezeichner aus enum-Klassen standardmäßig keine Operationen durchgeführt werden. Sollen zum Beispiel enum-Bezeichner miteinander verodern werden, so ist dazu eine entsprechende Funktion notwendig. 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 dann die Bezeichner auf den zugrundeliegenden Datentyp konvertiert, dann verodert und zum Schluss ein neues enum-Objekt mit der Ergebnis der Veroderung erzeugt und zurückgegeben.

PgmHeader// Enum-Klasse definieren
enum class Color {RED, GREEN, BLUE};

// Oder-Operator fuer Enum-Klasse definieren
constexpr Color operator | (Color col1, Color col2)
{
    return Color( static_cast<typename std::underlying_type<Color>::type>(col1) |
                  static_cast<typename std::underlying_type<Color>::type>(col2));
}

int main()
{
    // Farben verodern, 'ruft' den Operator | fuer
    // die enum-Klasse auf.

    Color newCol = Color::GREEN | Color::BLUE;
    ....
}

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

Überladen von logischen Operatoren

Oftmals ist es erforderlich, Objekte zu vergleichen. Da im C++ Standard nicht definiert ist, wie Objekte zu vergleichen sind, müssen hierfür die entsprechenden Operatoren überladen werden um z.B. folgende Anweisungen schreiben zu können:

if (comp1 == comp2)
    ...
if (comp1 != comp2)
    ....

Überladen durch Memberfunktion

Um einen logischen Operator durch eine nicht-statische Memberfunktion zu überladen, wird folgende Memberfunktion verwendet:

bool CANY::operator OSYMBOL (DTYP2 OP2);

Der Returntyp ist bei logischen Operatoren immer ein bool. OSYMBOL gibt wieder das Symbol des zu überladenden Operators (z.B. '==' für den Vergleich auf Gleichheit) für die Klasse CANY an. Da die Memberfunktion im Kontext des ersten, linken Operanden aufgerufen wird, gibt DTYP den Datentyp des zweiten, rechten Operanden an.

Sehen Sie sich nachfolgend einmal an, wie der != Operator überladen wurde. Er ruft den überladenen == Operator auf und negiert lediglich dessen Ergebnis. Verwenden Sie stets soweit wie möglich bereits bestehenden Code!

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    bool operator == (const Complex& op2) const;
    bool operator != (const Complex& op2) const;

};
// Überladener == Operator
bool Complex::operator == (const Complex& op2) const
{
    return ((real == op2.real) &&
            (imag == op2.imag));
}
// Überladener != Operator
bool Complex::operator != (const Complex& op2) const
{
    return !operator==(op2);
}

Soll der Operator durch eine Funktion überladen werden, ist analog zum Überladen von binären Operatoren vorzugehen. Lediglich der Returntyp der Funktion ist immer ein bool.

Überladen von unären Operatoren

Kommen wir nun zum Überladen von unären Operatoren wie z.B. dem NOT-Operator '!'. Auch diese Operatoren können entweder durch eine nicht-statische Memberfunktion oder eine Funktion überladen werden.

Überladen durch Memberfunktionen

Um einen unären Operator durch eine nicht-statische Memberfunktion zu überladen wird folgende Memberfunktion verwendet:

RVAL CANY::operator OSYMBOL ();

RVAL ist der Returntyp des Operators und OSYMBOL wiederum das Symbol des zu überladenden Operators (z.B. '!' für die NOT-Operation) der Klasse CANY. Da unäre Operatoren nur einen Operanden besitzen, benötigt die Memberfunktion auch keine weiteren Parameter.

Beispiel:

Für die bekannte 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 jedoch zwei Operanden und der Vorzeichenoperator nur einen. Da das Ergebnis des Vorzeichenoperators immer vom Typ des Operanden ist, muss die Operator-Memberfunktion auch ein Complex-Objekt zurückliefern. Im Beispiel kehrt der Vorzeichenoperator die Vorzeichen des Real- und Imaginäranteils um. Beim Überladen von Operatoren ist auch immer darauf zu achten, wann Operanden selbst verändern werden dürfen und wann nicht!

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex operator -() const;
};
// Überladener Vorzeichenoperator
Complex Complex::operator- () const
{
    Complex temp;
    temp.real = -real;
    temp.imag = -imag;
    return temp;
}

Überladen durch Funktionen

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

RVAL operator OSYMBOL (CAny& OP2);

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

Überladen wir den NOT-Operator '!' für die Klasse Complex durch eine normale 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- als auch der Imaginäranteil 0.0 enthält.

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
    ....
    // Funktion als friend-Funktion deklarieren
    friend bool operator ! (const Complex& op);
};
// Überladener NOT-Operator
bool operator! (const Complex& op)
{
   return ((op.real == 0.0) && (op.imag == 0.0));
}

Überladen der Operatoren ++, - - und des cast-Operators

Kommen wir nun zum Überladen von einigen besonderen Operatoren, welches uns auch noch im nächsten Kapitel beschäftigen wird.

Überladen der Operatoren ++ und - -

Einen Sonderfall beim Überladen von unären Operatoren stellen die Operatoren ++ und -- dar. Da sie sowohl als Präfix (++X) als auch als Suffix (X++) auftreten können, müssen hier beim Überladen zwei verschiedene Memberfunktionen verwendet werden.

Um den Präfix-Operator ++ zu überladen, wird die im nachfolgenden Beispiel angegebene Memberfunktion eingesetzt. Diese Memberfunktion muss eine Referenz auf das aktuelle Objekt zurückgeben, damit z.B. folgende Operation möglich ist:

comp2 = ++comp1;
PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex& operator ++();
};
// Überladener Präfix-Operator ++
Complex& Complex::operator++ ()
{
    ++real;
    ++imag;
    return *this;
}

Beachten Sie beim Präfix-Operator, dass zuerst die Addition ausgeführt und das Ergebnis der Addition zurückgeliefert wird. Außerdem verändert hier der Operator den Operanden (sprich das Objekt, in dessen Kontext die Memberfunktion aufgerufen wurde), sodass hier kein temporäres Hilfsobjekt, wie z.B. beim + Operator, benötigt wird.

Soll der Suffix-Operator überladen werden, so erhält die Memberfunktion einen Dummy-Parameter vom Typ int. Auch hier muss der überladene Operator wieder ein entsprechendes Complex-Objekt zurückliefern.

comp2 = comp1++;
PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex operator ++(int);
};
// Überladener Suffix-Operator ++
Complex Complex::operator++ (int)
{
    Complex temp(*this); //copy-ctor
    ++real;
    ++imag;
    return temp;
}

Beim Überladen dieses Operators ist zu beachten, dass der Ursprungswert des Objekts zurückgegeben werden muss, da der Suffix-Operator die Addition erst nach der Auswertung des Objekts durchführen darf.

AchtungBeachten Sie, dass diese Memberfunktion keine const-Memberfunktionen sind, da sie den Operanden ja verändern!

Überladener cast-Operator

Durch die Definition von cast-Operatoren kann dem Compiler mitgeteilt werden, wie ein Objekt in einen anderen Datentyp zu konvertieren ist. Die allgemeine Syntax für eine solche Typkonvertierung mittels einer nicht-statischen Memberfunktion lautet:

CANY::operator NEWDTYP ();

NEWDTYP gibt den Datentyp an, in den ein Objekt der Klasse CANY konvertiert werden soll. Am besten sehen wir uns dies auch wieder anhand eines Beispiels an.

Ausgangsbasis für dieses Beispiel ist wiederum die Klasse Complex. Ziel der Typkonvertierung ist es, folgende Anweisung schreiben zu können:

double val = comp;

wobei comp ein Objekt der Klasse Complex sein soll. Damit obige Anweisung durch den Compiler richtig aufgelöst werden kann, müssen wir ihm mitteilen, wie ein Complex-Objekt in einen double-Wert zu konvertieren ist. Bei dieser Konvertierung ist NEWDTYP der Datentyp double und damit ergibt sich die nachfolgend dargestellte Operator-Memberfunktion. Die Typkonvertierung erfolgt hier durch ziehen der Wurzel aus der Addition der quadrierten Anteile.

PgmHeaderclass Complex
{
    double real, imag;
    ....
  public:
    ....
    operator double () const;
};
// Typkonvertierung Complex -> double
Complex::operator double () const
{
    return static_cast<double>(sqrt(real*real + imag*imag));
}

Beachten Sie, dass die Operator-Memberfunktion keinen Returntyp besitzt, obwohl sie einen Wert zurückliefert, und eine const-Memberfunktion ist. Würden wir hier keine const-Memberfunktion verwenden, so könnten wir kein const Complex Objekt in einen double-Wert konvertieren.

const Objekte und überladene Operatoren

Sehen wir uns das Überladen von Operatoren jetzt noch etwas genauer an, und zwar im Zusammenhang mit const-Objekten. Die allgemeine Memberfunktion zum Überladen von Operatoren hat ja folgende Syntax:

RVAL CANY::operator OSYMBOL (DTYP);

Und wie bereits des Öfteren erwähnt, erfolgt der Aufruf des Operators immer im Kontext des linken Operanden und der rechte Operand wird als Parameter an die Memberfunktion übergeben. Nehmen wir jetzt einmal an, wir wollen für eine Klasse CAny den Plus-Operator überladen und hätten zwei Objekte dieser Klasse definiert, um folgende Operationen durchführen zu können:

CAny ncObject1, res;
const CAny cObject2;

res = ncObject1 + ncObject1;
res = ncObject1 + cObject2;
res = cObject2 + ncObject1;
res = cObject2 + cObject2;

Welche Operator-Memberfunktionen müssten wir letztendlich definieren, damit auch alle Operationen definiert sind?

Nachfolgend sind die Operator-Memberfunktionen in der Reihenfolge aufgeführt, in der sie für die obigen Operationen aufgerufen werden. Ist der zweite Operand ein const-Objekt, so muss der Parameter der Operator-Memberfunktion auch ein const sein. Ist dagegen der erste Operand ein const, so muss die Operator-Memberfunktion selbst const sein. Da ein const-Objekt laut Definition nicht verändert werden darf, darf es also nur Memberfunktionen aufrufen, die dies auch sicherstellen. Und dies sind const-Memberfunktionen.

PgmHeaderclass Complex
{
    double real;   // Realanteil
    double imag;   // Imaginäranteil
  public:
    ....
    Complex operator + (Complex& op2);
    Complex operator + (const Complex op2);
    Complex& operator + (Complex& op2) const;
    Complex& operator + (const Complex op2) const;
};

Aber keine Panik! Wir müssen nicht immer alle vier Operator-Memberfunktionen für überladene Operatoren definieren. Ein nicht-const Objekt kann jederzeit durch den Compiler in ein const-Objekt konvertiert werden (aber nicht umgekehrt). Und damit würde im Prinzip die letzte Memberfunktion alleine ausreichen. Ob dies aber für alle Operatoren ausreicht, hängt vom jeweiligen Anwendungsfall ab.

HinweisVergessen Sie einmal die Operator-Memberfunktion für ein const Objekt zu definieren und Sie rufen den Operator mit einem const Objekt auf, so erhalten Sie eine Fehlermeldung in der Art, dass kein Operator für const class CLASS definiert ist.

Sonstige Hinweise zum Überladen von Operatoren

Zum Schluss dieses Kapitels noch drei Hinweise:

  1. Das Überladen eines Operators ändert niemals die Rangfolge der Operatoren, d.h. es gilt z.B. auch weiterhin, dass eine Multiplikation immer vor einer Addition ausgeführt wird.
  2. Auch die Anzahl der Operanden eines Operators ist fest vorgegeben. So benötigt der Plus-Operator immer zwei Operanden.
  3. Haben Sie die Operatoren für = und * überladen, so können Sie trotzdem nicht die Kurzschreibweise *= verwenden. Sie müssen hierzu den Operator *= explizit überladen.

Und sollten Sie noch Zweifel haben, wann ein überladener Operator eine Referenz zurückliefern muss und wann ein Objekt, so können Sie sich an folgender Daumregel orientieren:

HinweisEin überladener Operator liefert in der Regel dann eine Referenz zurück, wenn der Operand selbst verändert wird. In allen anderen Fällen ist ein Objekt zurückzuliefern.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.