· 

Operator Overloading in C++

1. Einführung

Bereits in der Grundschule wirst du mit Operatoren konfrontiert. Für gewöhnlich lernst du dort die Klassiker \(+\), \(-\), \(\times\) und \(\div\) kennen. Im Verlauf deiner Mathematiker-Karriere wird dieses so früh erlernte Wissen allerdings infrage gestellt, denn wieso sollte man den \(+-\)Operator auf das bloße Addieren zweier Zahlen beschränken? Man könnte eine Addition doch auch auf Objekte erweitern und so z. B. die Summe zweier Vektoren bilden. Wenn du die Multiplikation, die du mit dem Mal-Punkt \(\cdot\) und dem Kreuz \(\times\) darstellen kannst, allgemein betrachtest, verbirgt sich dahinter eine Verknüpfung, die wie folgt definiert $$\cdot :M\times M\mapsto M, (a,b)\mapsto a\cdot b$$ wobei \(M\) eine Menge ist. Wenn \(M\) z. B. die reellen Zahlen \(\mathbb{R}\) sind, dann kannst du via $$\cdot:\mathbb{R}\mapsto\mathbb{R}, (a,b)\mapsto a\cdot b$$ die Multiplikation auf den reellen Zahlen definieren. Doch worauf beruft sich die Uni bei dem Mal-Punkt, der hinter dem geordneten Paar \((a,b)\) auftaucht? Nun, dafür hast du in der Grundschule mal eine riesige Verknüpfungstabelle kennengelernt, in der definiert wurde, dass z. B. \(2\cdot 4=8\) oder \(5\cdot 5=25\) ist. Man ist bei den Mengen jedoch nicht auf die reellen Zahlen festgelegt und so kann der Multiplikationsoperator in verschiedenen Situationen unterschiedliche Bedeutungen haben. Und genau so ist es auch in der Programmierung. Deshalb bietet die Programmiersprache C++ die Möglichkeit, Operatoren zu überladen. Im Englischen nennt man das Operator Overloading.

2. Welche Operatoren können überladen werden?

Mit Hilfe des Überladens von Operatoren kannst du für Objekte selbst definierter Klassen (fast) alle Operatoren mit einer neuen Bedeutung versehen. Diese Bedeutung kapselst du in speziellen Funktionen bzw. Methoden. Der Grundgedanke ist (wie der Name bereits vermuten lässt) dem des Überladens von Funktionen bzw. Methoden sehr ähnlich.

Insgesamt können diese \(42\) Operatoren überladen werden:
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new [] delete delete []

Wenn du z. B. den >-Operator überlädst, könntest du deine Kollegen trollen und ihm die Funktionalität "kleiner als" (<) zuweisen. Du könntest aber auch den Inkrement-Operator ++ dadurch so umschreiben, dass er statt 1 jeweils 2 auf die Zahl addiert, hinter der er steht. Du solltest aber tunlichst davon absehen, den Zugriffsoperator -> zu überladen, da das ganz gruselige Effekte in der späteren Programmierung haben kann (insbesondere dann, wenn einer deiner Kollegen deinen Code warten bzw. weiterentwickeln darf). Stelle dir (und das gilt grundsätzlich) am besten vor, dass die Kollegen, die mit deinem Code arbeiten sollen, gewaltbereite Psychopathen sind, die deine Adresse haben. Dann wirst du recht schnell selbstständig vom Überladen dieses Operators absehen.

Nicht überladen werden können übrigens die vier Operatoren
::
.
.*
und der Elvis-Operator :?/.

Für jeden dieser Operatoren ein eigenes Code-Beispiel bzw. ein sinnvolles Einsatzszenario zu finden, würde den Rahmen dieses Artikels sprengen. Du solltest dennoch wissen, das die Syntax zum Überladen einzelner Operatoren vom Operator-Typ abhängt. Binäre Operatoren, also Operatoren, die zwischen zwei Operanden stehen (wie bspw. der Modulo-Operator %), werden anders überladen als unäre Operatoren, die vor einem Operanden stehen (wie der für die logische Verneinung). Manche Operatoren sind (je nach Kontext) binär oder unär (oder auch nichts von beidem, wie z. B. der new-Operator). Der * bedeutet binär die Multiplikation zweier Variablen, während er unär einer Variable vorangestellt eine Dereferenzierung bewirkt. Achte also auf die Compiler-Hinweise oder siehe in den Docs nach.

Die von C++ vorgegebene Operatorrangfolge (also die Bindungsstärken bzw. die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden) bleibt übrigens erhalten und kann nicht geändert werden. Das logische NICHT ! bindet also stärker als z. B. das logische UND &&.


3. Überladen des +- und <<-Operators

Du sollst nun anhand eines praktischen Beispiels, nämlich für eine Vektorklasse, lernen, wie du die Operatoren + und << (für die Ausgabe) überladen kannst. Du bist damit in der Lage, Vektoren auf eine intuitive Art und Weise im Code zu addieren bzw. durch das Schreiben auf den Ausgabestrom anzuzeigen.

Für die Vektorklasse importierst du dir zunächst <iostream> und legst den Namespace std fest, um im Code nicht an verschiedenen Stellen ständig das std voranstellen zu müssen. Dann schreibst du eine einfache Vektor-Klasse, die drei Double-Werte zur Angabe der \(x-\), \(y-\) und \(z-\)Koordinate enthält. Diese werden im Konstruktor der Klasse übergeben:
class Vector{
 public:
 double x;
 double y;
 double z;

 Vector(double x, double y, double z): x(x), y(y), z(z) {}
};
In der Main kannst du dir nun zwei Vektoren \(v_1=\left(\begin{matrix}1\\2\\3\end{matrix}\right)\) und \(v_2=\left(\begin{matrix}4\\4\\0\end{matrix}\right)\) definieren und diese dann addieren:
int main(){
 Vector v1(1,2,3);
 Vector v2(4,4,0);
 Vector v3 = v1 + v2; 
}
Wenn du diesen Code nun ausführst, läufst du direkt in eine Fehlermeldung, da C++ den Operator \(+\) nicht für die von dir definierte Vektorklasse kennt. Diesen musst du erst implementieren. Dafür definierst du dir eine Funktion, die mit dem Keyword operator eingeleitet wird. Überladene Operatorfunktionen besitzen als Rückgabetyp den Typ der Klasse, in der sie überladen werden (in diesem Fall also Vector, da der \(+-\)Operator für Vektoren überladen werden soll). An das Keyword operator folgt der Operator, der überladen werden soll (hier das \(+\)). Du erinnerst dich, dass ich geschrieben habe, dass die Signatur davon abhängt, ob z. B. ein binärer oder unärer Operator vorliegt, oder? Das kommt hier jetzt zum Tragen. Die Signatur für einen binären Operator erfordert einen Übergabeparameter vom Typ deiner Klasse (hier Vector).
Innerhalb der Funktion definierst du dir einen Ergebnisvektor und berechnest die einzelnen Koordinaten des neuen Vektors durch Addition der jeweiligen Koordinaten des übergebenen Vektors zu den Objektvariablen. Du musst die Sicht eines Vektors einnehmen, auf den ein anderer Vektor addiert wird. Zum Schluss gibst du den Ergebnisvektor zurück. Es werden für einen binären Operator also nicht zwei, sondern nur ein Parameter benötigt. Für einen unären Operator dementsprechend keiner. Das this könntest du auch weglassen.
Vector operator+ (const Vector &vector){
  Vector result(0,0,0);
  result.x = this->x + vector.x;
  result.y = this->y + vector.y;
  result.z = this->z + vector.z;
  return result;
}
Wenn du das Programm jetzt erneut ausführst, ist die Fehlermeldung von vorhin verschwunden. Allerdings weißt du jetzt noch nicht, ob die Addition auch zum richtigen Ergebnis, also dem Vektor $$v_3=\left(\begin{matrix}1\\2\\3\end{matrix}\right)+\left(\begin{matrix}4\\4\\0\end{matrix}\right)=\left(\begin{matrix}5\\6\\3\end{matrix}\right)$$ führt. Statt einer print()-Fuktion, kannst du auch den Operator << überladen und deine Vektoren direkt in menschenlesbarer Form auf den Ausgabestrom schreiben. Du überlädst dabei jedoch nicht den <<-Operator der Klasse Vector (diesen gibt es nämlich nicht), sondern den der Klasse Output-Stream (ostream). Deshalb musst du als Rückgabetyp ostream& verwenden. Als Übergabeparameter erhält dieser Operator eine Referenz auf den Output-Stream und eine auf den Vektor, der ausgegeben werden soll. Dann baust du dir die gewünschte Darstellung zusammen und gibst am Ende den Output-Stream zurück.
ostream& operator<<(ostream& stream, const Vector& vector){
  stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
  return stream;
};
Wenn du den Vektor jetzt mit diesem überladenen Operator durch diese Zeile in der main()
cout << v3;
ausgeben willst, sollte eigentlich "(5,6,3)" auf die Konsole gedruckt werden. Stattdessen wirst du mit einer Fehlermeldung bestraft. Das liegt daran, dass du den Funktion zum Übleraden deines Ausgabeoperators innerhalb der Vektorklasse als friend definieren musst.
friend ostream& operator<<(ostream& stream, const Vector& vector){
  stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
  return stream;
};

Alternativ kannst du die Funktion auch einfach außerhalb deiner Klasse definieren und solltest dann auf das friend verzichten. Beim Ausführen dieses Programms wird nun der richtige Wert ausgegeben.

Das Überladen von Operatoren bietet dir die Möglichkeit, die visuelle Darstellung deines Programms intuitiver zu gestalten.

Das Testprogramm sieht dann final wie folgt aus:

#include <iostream>

using namespace std;

class Vector{
 public:
 double x;
 double y;
 double z;

Vector(double x, double y, double z): x(x), y(y), z(z) {}

Vector operator+ (const Vector &vector){
  Vector result(0,0,0);
  result.x = this->x + vector.x;
  result.y = this->y + vector.y;
  result.z = this->z + vector.z;
  return result;
}

friend ostream& operator<<(ostream& stream, const Vector& vector){
  stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
  return stream;
};

};

int main(){
 Vector v1(1,2,3);
 Vector v2(4,4,0);
 Vector v3 = v1 + v2; 
 cout << v3;
}