Allgemeines Überladen von Operatoren
Für Objekte 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:
- durch eine nicht-statische Methode oder
- durch eine Funktion, welche in der Regel als friend-Funktion definiert ist (friend-Funktionen werden im Kapitel friend Funktionen & Klassen genauer 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 die Klasse CData soll der Plus-Operator definiert werden, sodass folgende Operation möglich ist:
data3 = data1 + data2;
Da eine Addition von zwei CData-Objekten wieder ein CData-Objekt ergibt, muss die Methode ein CData-Objekt zurückgeben, welches das Ergebnis enthält. Damit besitzt die Methode folgende Deklaration:
CData operator + (const CData& op2) const;
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 zweier CData-Objekte ergibt ein neues CData-Objekt das beide Daten enthält.
// CData Objekte addieren
CData CData::operator + (const CData& op2) const
{
CData res; // Ergebnisobjekt
// Datenfeldgroesse berechnen und anlegen
res.dSize = dSize+op2.dSize;
res.pData = new int[res.dSize];
// Daten aus Operand1 ins Ergebnisfeld
auto res_index = 0;
for (size_t index=0; index<dSize; index++)
res.pData[res_index++] = pData[index];
// Daten aus Operand2 anhaengen
for (size_t index=0; index<op2.dSize; index++)
res.pData[res_index++] = op2.pData[index];
// Ergebnisobjekt zurueckgeben
return res;
}
Zunächst wird ein lokales Objekt der Klasse CData definiert, dessen Datenfeld so groß ist, dass die Daten aus den beiden Operanden abgelegt werden können. Anschließend werden die Datenfelder der Operanden in der richtigen Reihenfolge in das Datenfeld des temporären CData-Objekts übernommen. Es muss hier unbedingt ein temporäres Hilfsobjekt zu Hilfe genommen werden, denn eine Addition verändert nicht den Inhalt der Operanden!
Beachten Sie, dass hier das temporäre Objekt zurückgeliefert wird und nicht nur Referenz darauf!
Wenn die im Anhang Q aufgeführte CData-Klasse um die Methode operator + erweitert wird, könnte eine Anwendung wie folgt aussehen:
import CData;
int main()
{
// Zwei CData Objekte definieren und ausgeben
CData data1{5}, data2{3};
data1.Print();
data2.Print();
// Neue CData Objekt definieren und ihm die Summe
// aus den beiden anderen CData Objekten zuweisen
CData data3 = data1+data2;
// Neue CData Objekt ausgeben
data3.Print();
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Daten Objekt 2:
24, 78, 58,
Daten Objekt 3:
41, 67, 34, 0, 69, 24, 78, 58,
Sehen wir uns ein weiteres Beispiel für das Überladen des Plus-Operators der Klasse CData an. Anstelle zwei CData-Objekte zu addieren, soll jetzt zu jedem Datum im CData-Objekt ein int-Wert addiert werden, d.h., folgende Operation soll erlaubt sein:
data2 = data1 + 10;
Der Returntyp der Methode ändert sich nicht, da auch hier das Ergebnis der Addition ein CData-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 int.
// Addiere int-Wert auf Daten
CData CData::operator + (this const CData& self, int value)
{
// Ausgangsdaten in temp. Objekt uebernehmen
CData tmp(self);
// Daten um uebergebenen Wert erhoehen
for (size_t index=0; index<tmp.dSize; index++)
tmp.pData[index] += value;
// Ergebnis zurueckgeben
return tmp;
}
Und die Anwendung dazu:
import CData;
int main()
{
// CData Objekt definieren und ausgeben
CData data1{5};
data1.Print();
// Alle Daten um 10 erhoehen
CData data2 = data1 + 10;
data2.Print();
}
Die umgekehrte Operation
data2 = 10 + 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 int. Da für die intrinsischen Datentypen keine Operatoren überladen werden können, muss für diese Operation 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.
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 CData durch eine Funktion überladen wird, um zu einem int-Wert ein CData-Objekt zu addieren. Beachten Sie bitte die Deklaration der friend-Funktion innerhalb der Klasse CData. Die friend-Deklaration gleich bis auf das vorangestellte Schlüsselwort friend der Deklaration einer Member-Operatorfunktion.
Achten Sie darauf, dass die Operatorfunktion auch exportiert wird wenn Sie mit Modulen arbeiten. Eine friend-Deklaration in einer exportierten Klasse exportiert nicht automatisch die dazugehörige Funktion.
export module CData;
export class CData
{
...
public:
....
// Summiere alle Daten zu einem int-Wert
friend int operator + (int value, const CData& obj);
};
// Summiere alle Daten zu einem int-Wert
export int operator + (int value, const CData& obj)
{
int res = value;
for (size_t index=0; index<obj.dSize; index++)
res += obj.pData[index];
return res;
}
Und die Anwendung dazu:
#include <print>
import CData;
int main()
{
// CData Objekt definieren und ausgeben
CData data1{5};
data1.Print();
// Summe der Objektdaten plus 100
auto sum = 100 + data1;
std::println("Summe der Daten+100: {}",sum);
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Summe der Daten+100: 311
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 eine entsprechende 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.
#include <utility>
#include <print>
// Die enum-Klasse fuer Fensterstile
enum class Style {FRAME=0x01, MENU=0x02, CLOSEBOX=0x04};
// Oder-Operator fuer enum-Klasse
constexpr Style operator | (Style s1, Style s2)
{
return Style (std::to_underlying(s1) |
std::to_underlying(s2));
}
int main()
{
Style myStyle = Style::FRAME | Style::CLOSEBOX;
std::println("myStyle: {:x}", std::to_underlying(myStyle));
}
myStyle: 5
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 CData soll der NOT-Operator überladen werden, sodass folgende Operation definiert ist:
bool res = !cdataObj;
Im Beispiel soll der Operator als bool-Wert zurückgeben, ob im CData-Objekt Daten abgespeichert sind oder nicht.
>// Prueft ob Daten abgespeichert sind
bool CData::operator !() const
{
return pData == nullptr;
}
Und die Anwendung dazu:
#include <print>
import CData;
int main()
{
CData obj1{5}, obj2;
// Pruefen ob Objekte Daten enthalten
if (!obj1)
std::println("Keine Daten in obj1");
if (!obj2)
std::println("Keine Daten in obj2");
}
Keine Daten in obj2
Ü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 CData durch eine Funktion:
export module CData;
export class CData
{
...
public:
....
// Prueft ob Daten abgespeichert sind
friend bool operator !(const CData& obj);
};
// Prueft ob Daten abgespeichert sind
export bool operator !(const CData& obj)
{
return obj.pData == nullptr;
}
Vergleichsoperatoren (spaceship operator)
Sollen Objekte verglichen werden, sind hierfür entsprechende Vergleichsoperatoren zu definieren.
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. Seit C++20 muss prinzipiell nur noch der Operator <=>, auch spaceship-Operator oder 3-Wege-Vergleich genannt, definiert werden. Sollen Objekte auch auf Gleichheit verglichen werden, ist zusätzlich noch der Vergleichsoperator == zu definieren.
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 aber 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 Art, 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 CData an:
export module CData;
export class CData
{
...
public:
....
// space-operator
auto operator <=> (const CData& op2) const = default;
};
Und die Anwendung:
#include <print>
import CData;
int main()
{
CData obj1, obj2;
// Pruefen ob Objekte identisch sind
if (obj1 == obj2)
std::println("obj1 == obj2");
else
std::println("obj1 != obj2");
}
obj1 != obj2
Obwohl beide Objekte keine Daten enthalten und auf den ersten Blick identisch sein sollten, liefert der Vergleich false zurück. Der Grund hierfür ist, dass die Objekte ungleiche Objektnummern enthalten.
Sollen nur die Daten 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.
// space-operator
auto CData::operator <=> (this const CData& self, const CData& op2)
{
// Zuerst die Groesse vergleichen
if (auto cmp = self.dSize <=> op2.dSize; cmp != 0)
return cmp;
// Nun die Daten vergleichen
// Aufruf des operators ==
return (self == op2) <=> true;
}
// Vergleich auf Gleichheit
bool CData::operator == (const CData& op2) const
{
if (dSize != op2.dSize)
return false;
// Nun die Daten vergleichen
bool isEqual = true;
for (size_t index = 0; index < dSize; index++)
{
// Wenn Daten ungleich, Schleife verlassen
if (pData[index] != op2.pData[index])
{
isEqual = false;
break;
}
}
return isEqual;
}
Und auch dazu wieder eine Beispiel-Anwendung:
#include <print>
import CData;
int main()
{
CData obj1{ 5 }; // beliebiges CData Objekt
CData obj2{ obj1 }; // Kopie von obj1
CData obj3{ 3 }; // beliebiges CData Objekt
// Ausgabe der Daten
obj1.Print();
obj2.Print();
obj3.Print();
// Objekte auf Gleichheit vergleichen
// Ruft nur den operator == auf
if (obj1 == obj2)
std::println("obj1 == obj2");
else
std::println("ob1 != obj2");
if (obj1 == obj3)
std::println("obj1 == obj3");
else
std::println("obj1 != obj3");
// Objekte auf kleiner als vergleichen
// op1 < op2 wenn die Feldgroesse von op1 < op2
// Ruft nun den spaceship operator auf
if (obj1 < obj3)
std::println("obj1 enhaelt weniger Daten als obj2");
else
std::println("obj1 enthaelt gleich/mehr Daten als obj2");
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Daten Objekt 2:
41, 67, 34, 0, 69,
Daten Objekt 3:
5, 45, 81,
obj1 == obj2
obj1 != obj3
obj1 enthaelt gleich/mehr Daten als obj2
Überladen durch Funktion
Soll der spaceship-Operator durch eine Funktion überladen werden, ist analog zum Überladen von binären Operatoren vorzugehen. Es ist nur darauf zu achten, dass beide Operatorfunktionen exportiert werden müssen wenn sie in einem Modul definiert sind.
export module CData;
export class CData
{
...
public:
....
// Summiere alle Daten zu einem int-Wert
// space-operator
friend auto operator <=> (const CData& op1, const CData& op2);
// Vergleich auf Gleichheit
friend bool operator == (const CData& op1, const CData& op2);
};
// space-operator
export auto operator <=> (const CData& op1, const CData& op2)
{
// Zuerst die Groesse vergleichen
if (auto cmp = op1.dSize <=> op2.dSize; cmp != 0)
return cmp;
// Nun die Daten vergleichen
// Aufruf des operators ==
return (op1 == op2) <=> true;
}
// Vergleich auf Gleichheit
export bool operator == (const CData& op1, const CData& op2)
{
if (op1.dSize != op2.dSize)
return false;
// Nun die Daten vergleichen
bool isEqual = true;
for (size_t index = 0; index < op1.dSize; index++)
{
// Wenn Daten ungleich, Schleife verlassen
if (op1.pData[index] != op2.pData[index])
{
isEqual = false;
break;
}
}
return isEqual;
}
const-Objekte und überladene Operatoren
Bekanntermaßen ist die allgemeine Syntax für das Überladen von Operatoren durch eine Methode
RTYP CANY::operator OSYMBOL (DTYP val);
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 die Klasse CData der Plus-Operator zu überladen, um die nachfolgenden Operationen durchführen zu können:
CData ncObj1, res; // nicht-konstantes Objekt
const CData cObj2; // konstantes Objekt
res = ncObj1 + ncObj1; // non_const + non_const
res = ncObj1 + cObj2; // non_const + const
res = cObj2 + ncObj1; // const + non_const
res = cObj2 + cObj2; // 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.
export module CData;
export class CData
{
...
public:
....
CData operator + (CData& op2);
CData operator + (const CData& op2);
CData operator + (CData& op2) const;
CData operator + (const CData& op2) const;
};
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.
Übungen
ogen_01:
Erstellen Sie eine Klasse zum Abspeichern von Rechteckdaten, bestehend aus einer Position (x,y) sowie einer Ausdehnung (width,height).
Außer dem notwendigen Konstruktor zur Initialisierung der Rechteckdaten und einer Methode PrintRect() für die Ausgabe der Daten, sollen folgende Operationen mit einem Rechteck möglich sein:
| Operator | Funktion |
|---|---|
| r1 & r2 | Verundet zwei Rechtecke. Hierbei wird die gemeinsame Fläche (Schnittfläche) der beiden Rechtecke berechnet und als Rechteck-Objekt zurückgegeben. |
| r1 == r2 | Vergleicht, ob zwei Rechtecke identisch sind, d.h. die gleiche Position und Ausdehnung besitzen. |
| !r | Stellt fest, ob ein Rechteck-Objekt eine Fläche besitzt. |
In der main() Funktion sind zwei nicht identische Rechtecke dynamisch zu erstellen. Vergleichen Sie die beiden Rechtecke und geben das Ergebnis der Überprüfung aus.
Aus den beiden Rechtecken ist mittels des & Operators die Schnittfläche zu bilden und diese einem weiteren Rechteck-Objekt zuzuweisen.
Zum Schluss ist zu prüfen, ob die ermittelte Schnittfläche leer ist oder nicht und das Ergebnis auszugeben
1. Rechteck:
Rechteck auf (10,20), Groesse: (100,200)
2. Rechteck:
Rechteck auf (50,50), Groesse: (100,200)
Rechtecke sind unterschiedlich
Rechtecke ueberschneiden sich! Gemeinsame Flaeche:
Rechteck auf (50,50), Groesse: (60,170)
ogen_02:
Erweitern Sie die in der vorherigen Übung oassign_02 erstellte Klasse CString um den Operator <=>, um CString-Objekte vergleichen zu können.
Ersetzen Sie anschließend die beiden AddString() Methoden der Klasse durch entsprechend überladene Operatoren.
Erstellen Sie im main() zwei CString-Objekte, wobei die beiden CString-Objekte bei ihrer Definition mit beliebigen, unterschiedlichen Texten zu initialisieren sind. Geben Sie beide Objekte aus.
Addieren Sie das erste und zweite CString-Objekt und weisen Sie das Ergebnis einem weiteren CString-Objekt zu. Geben Sie das neue CString-Objekt aus.
Vergleichen Sie das erste CString-Objekt mit dem Zweiten auf größer und geben das Ergebnis der Abfrage aus.
Zum Schluss weisen Sie dem ersten CString-Objekt das zweite CString-Objekt zu und vergleichen beide Objekte erneut.
Ausgangs-Strings
S1: Eine Investition
S2: in Wissen
S3 (S1+s2): Eine Investition in Wissen
S1>S2: false
Nach S1=S2:
S1: in Wissen
S2: in Wissen
S1==S2: true