Operatorüberladung
Passt die C++ Operatoren für Operanden von benutzerdefinierten Typen an.
[edit] Syntax
Operatorfunktionen sind Funktionen mit speziellen Funktionsnamen
operator op |
(1) | ||||||||
operator newoperator new [] |
(2) | ||||||||
operator deleteoperator delete [] |
(3) | ||||||||
operator co_await |
(4) | (seit C++20) | |||||||
| op | - | einer der folgenden Operatoren:+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(seit C++20) && || ++ -- , ->* -> () [] |
Das Verhalten von Nicht-Satzzeichen-Operatoren wird auf ihren eigenen Seiten beschrieben. Sofern nicht anders angegeben, gilt die übrige Beschreibung auf dieser Seite nicht für diese Funktionen.
[edit] Erklärung
Wenn ein Operator in einem Ausdruck erscheint und mindestens einer seiner Operanden einen Klassentyp oder einen Aufzählungstyp hat, dann wird die Überladungsauflösung verwendet, um die benutzerdefinierte Funktion zu bestimmen, die unter allen Funktionen aufgerufen wird, deren Signaturen den folgenden entsprechen
| Ausdruck | Als Memberfunktion | Als Nicht-Memberfunktion | Beispiel |
|---|---|---|---|
| @a | (a).operator@ ( ) | operator@ (a) | !std::cin ruft std::cin.operator!() auf |
| a@b | (a).operator@ (b) | operator@ (a, b) | std::cout << 42 ruft std::cout.operator<<(42) auf |
| a=b | (a).operator= (b) | kann kein Nicht-Member sein | Gegeben std::string s;, ruft s = "abc"; s.operator=("abc") auf |
| a(b...) | (a).operator()(b...) | kann kein Nicht-Member sein | Gegeben std::random_device r;, ruft auto n = r(); r.operator()() auf |
| a[b...] | (a).operator[](b...) | kann kein Nicht-Member sein | Gegeben std::map<int, int> m;, ruft m[1] = 2; m.operator[](1) auf |
| a-> | (a).operator->( ) | kann kein Nicht-Member sein | Gegeben std::unique_ptr<S> p;, ruft p->bar() p.operator->() auf |
| a@ | (a).operator@ (0) | operator@ (a, 0) | Gegeben std::vector<int>::iterator i;, ruft i++ i.operator++(0) auf |
|
In dieser Tabelle ist | |||
|
Zusätzlich werden für Vergleichsoperatoren ==, !=, <, >, <=, >=, <=>, bei der Überladungsauflösung auch die umgeschriebenen Kandidaten operator== oder operator<=> berücksichtigt. |
(seit C++20) |
Überladene Operatoren (aber nicht die eingebauten Operatoren) können in Funktionsschreibweise aufgerufen werden
std::string str = "Hello, "; str.operator+=("world"); // same as str += "world"; operator<<(operator<<(std::cout, str), '\n'); // same as std::cout << str << '\n'; // (since C++17) except for sequencing
Statisch überladene OperatorenÜberladene Operatoren, die Memberfunktionen sind, können als statisch deklariert werden. Dies ist jedoch nur für operator() und operator[] zulässig. Solche Operatoren können in Funktionsschreibweise aufgerufen werden. Wenn diese Operatoren jedoch in Ausdrücken vorkommen, benötigen sie immer noch ein Objekt vom Klassentyp. struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(seit C++23) |
[edit] Beschränkungen
- Eine Operatorfunktion muss mindestens einen Funktionsparameter oder einen impliziten Objektparameter haben, dessen Typ eine Klasse, eine Referenz auf eine Klasse, eine Aufzählung oder eine Referenz auf eine Aufzählung ist.
- Die Operatoren
::(Gültigkeitsbereichsauflösung),.(Mitgliedszugriff),.*(Mitgliedszugriff über Zeiger auf Mitglied) und?:(ternärer Bedingungsoperator) können nicht überladen werden. - Neue Operatoren wie
**,<>oder&|können nicht erstellt werden. - Es ist nicht möglich, die Priorität, Gruppierung oder Anzahl der Operanden von Operatoren zu ändern.
- Die Überladung von operator
->muss entweder einen Rohzeiger zurückgeben oder ein Objekt (per Referenz oder per Wert) zurückgeben, für das operator->seinerseits überladen ist. - Die Überladungen der Operatoren
&&und||verlieren die Kurzschlussauswertung.
|
(bis C++17) |
[edit] Kanonische Implementierungen
Abgesehen von den oben genannten Einschränkungen macht die Sprache keine weiteren Vorgaben, was die überladenen Operatoren tun oder welchen Rückgabetyp sie haben (dieser nimmt nicht an der Überladungsauflösung teil). Im Allgemeinen wird jedoch erwartet, dass überladene Operatoren sich so ähnlich wie möglich wie die eingebauten Operatoren verhalten: operator+ soll seine Argumente addieren und nicht multiplizieren, operator= soll zuweisen usw. Verwandte Operatoren sollen sich ähnlich verhalten (operator+ und operator+= führen dieselbe additionsähnliche Operation durch). Die Rückgabetypen sind durch die Ausdrücke begrenzt, in denen der Operator verwendet werden soll: Zuweisungsoperatoren geben beispielsweise per Referenz zurück, um das Schreiben von a = b = c = d zu ermöglichen, da die eingebauten Operatoren dies zulassen.
Häufig überladene Operatoren haben die folgenden typischen, kanonischen Formen:[1]
[edit] Zuweisungsoperator
Der Zuweisungsoperator operator= hat spezielle Eigenschaften: siehe Kopierzuweisung und Verschiebungszuweisung für Details.
Der kanonische Kopierzuweisungsoperator soll sicher bei Selbstzuweisung sein und die linke Seite (lhs) per Referenz zurückgeben
// copy assignment T& operator=(const T& other) { // Guard self assignment if (this == &other) return *this; // assume *this manages a reusable resource, such as a heap-allocated buffer mArray if (size != other.size) // resource in *this cannot be reused { temp = new int[other.size]; // allocate resource, if throws, do nothing delete[] mArray; // release resource in *this mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
|
Die kanonische Verschiebungzuweisung soll das verschobene Objekt in einem gültigen Zustand hinterlassen (d. h. einen Zustand mit intakten Klasseninvarianten) und entweder nichts tun oder zumindest das Objekt bei Selbstzuweisung in einem gültigen Zustand hinterlassen und die linke Seite (lhs) per Referenz auf nicht-const zurückgeben und noexcept sein // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(seit C++11) |
In Situationen, in denen die Kopierzuweisung nicht von der Wiederverwendung von Ressourcen profitieren kann (sie verwaltet kein heap-allokiertes Array und hat kein (möglicherweise transitives) Mitglied, das dies tut, wie z. B. ein Mitglied std::vector oder std::string), gibt es eine beliebte praktische Abkürzung: den Kopier-und-Tausch-Zuweisungsoperator, der seinen Parameter per Wert entgegennimmt (und somit sowohl als Kopier- als auch als Verschiebungzuweisung fungiert, abhängig von der Wertkategorie des Arguments), mit dem Parameter tauscht und den Destruktor die Bereinigung übernehmen lässt.
// copy assignment (copy-and-swap idiom) T& T::operator=(T other) noexcept // call copy or move constructor to construct other { std::swap(size, other.size); // exchange resources between *this and other std::swap(mArray, other.mArray); return *this; } // destructor of other is called to release the resources formerly managed by *this
Diese Form bietet automatisch eine starke Ausnahmegarantie, verbietet aber die Wiederverwendung von Ressourcen.
[edit] Stream-Extraktion und -Einfügung
Die Überladungen von operator>> und operator<<, die einen std::istream& oder std::ostream& als linkes Argument nehmen, werden als Einfügungs- und Extraktionsoperatoren bezeichnet. Da sie den benutzerdefinierten Typ als rechtes Argument (b in a @ b) nehmen, müssen sie als Nicht-Member implementiert werden.
std::ostream& operator<<(std::ostream& os, const T& obj) { // write obj to stream return os; } std::istream& operator>>(std::istream& is, T& obj) { // read obj from stream if (/* T could not be constructed */) is.setstate(std::ios::failbit); return is; }
Diese Operatoren werden manchmal als friend-Funktionen implementiert.
[edit] Funktionsaufrufoperator
Wenn eine benutzerdefinierte Klasse den Funktionsaufrufoperator operator) überlädt, wird sie zu einem FunctionObject-Typ.
Ein Objekt eines solchen Typs kann in einem Funktionsaufrufausdruck verwendet werden
// An object of this type represents a linear function of one variable a * x + b. struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // Represents function 2x + 1. Linear g{-1, 0}; // Represents function -x. // f and g are objects that can be used like a function. double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
Viele Standardbibliotheks-Algorithmen akzeptieren FunctionObjects, um das Verhalten anzupassen. Es gibt keine besonders hervorzuhebenden kanonischen Formen von operator), aber zur Veranschaulichung der Verwendung
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
Ausgabe
The sum is 15
[edit] Inkrement und Dekrement
Wenn der Postfix-Inkrement- oder -Dekrementoperator in einem Ausdruck vorkommt, wird die entsprechende benutzerdefinierte Funktion (operator++ oder operator--) mit einem ganzzahligen Argument 0 aufgerufen. Typischerweise wird sie als T operator++(int) oder T operator--(int) deklariert, wobei das Argument ignoriert wird. Die Postfix-Inkrement- und -Dekrementoperatoren werden normalerweise in Bezug auf die Präfixversionen implementiert
struct X { // prefix increment X& operator++() { // actual increment takes place here return *this; // return new value by reference } // postfix increment X operator++(int) { X old = *this; // copy old value operator++(); // prefix increment return old; // return old value } // prefix decrement X& operator--() { // actual decrement takes place here return *this; // return new value by reference } // postfix decrement X operator--(int) { X old = *this; // copy old value operator--(); // prefix decrement return old; // return old value } };
Obwohl die kanonischen Implementierungen der Präfix-Inkrement- und -Dekrementoperatoren per Referenz zurückgeben, ist der Rückgabetyp wie bei jeder Operatorüberladung benutzerdefiniert; beispielsweise geben die Überladungen dieser Operatoren für std::atomic per Wert zurück.
[edit] Binäre arithmetische Operatoren
Binäre Operatoren werden typischerweise als Nicht-Member implementiert, um die Symmetrie zu wahren (z. B. beim Addieren einer komplexen Zahl und einer Ganzzahl, wenn operator+ eine Memberfunktion des komplexen Typs ist, dann würde nur complex + integer kompiliert werden, und nicht integer + complex). Da für jeden binären arithmetischen Operator ein entsprechender zusammengesetzter Zuweisungsoperator existiert, werden kanonische Formen von binären Operatoren in Bezug auf ihre zusammengesetzten Zuweisungen implementiert
class X { public: X& operator+=(const X& rhs) // compound assignment (does not need to be a member, { // but often is, to modify the private members) /* addition of rhs to *this takes place here */ return *this; // return the result by reference } // friends defined inside class body are inline and are hidden from non-ADL lookup friend X operator+(X lhs, // passing lhs by value helps optimize chained a+b+c const X& rhs) // otherwise, both parameters may be const references { lhs += rhs; // reuse compound assignment return lhs; // return the result by value (uses move constructor) } };
[edit] Vergleichsoperatoren
Standardbibliotheksalgorithmen wie std::sort und Container wie std::set erwarten, dass für benutzerdefinierte Typen standardmäßig operator< definiert ist, und erwarten, dass dieser eine strikte schwache Ordnung implementiert (wodurch die Compare-Anforderungen erfüllt werden). Eine idiomatische Methode zur Implementierung einer strikten schwachen Ordnung für eine Struktur ist die Verwendung des lexikographischen Vergleichs, der von std::tie bereitgestellt wird
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // keep the same order } };
Typischerweise werden, sobald operator< bereitgestellt ist, die anderen relationalen Operatoren in Bezug auf operator< implementiert.
inline bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ } inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
Ebenso wird der Ungleichheitsoperator typischerweise in Bezug auf operator== implementiert
inline bool operator==(const X& lhs, const X& rhs) { /* do actual comparison */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
Wenn ein Drei-Wege-Vergleich (wie std::memcmp oder std::string::compare) bereitgestellt wird, können alle sechs Zwei-Wege-Vergleichsoperatoren daraus ausgedrückt werden
inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }
[edit] Array-Indexoperator
Benutzerdefinierte Klassen, die einen array-ähnlichen Zugriff bieten, der sowohl Lesen als auch Schreiben ermöglicht, definieren typischerweise zwei Überladungen für operator[]: const- und non-const-Varianten
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
|
Alternativ können sie als einzelne Member-Funktionsschablone mit einem expliziten Objektparameter ausgedrückt werden struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(seit C++23) |
Wenn der Werttyp als skalarer Typ bekannt ist, sollte die const-Variante per Wert zurückgeben.
Wo ein direkter Zugriff auf die Elemente des Containers nicht gewünscht oder nicht möglich ist oder zwischen der lvalue c[i] = v; und der rvalue v = c[i]; Verwendung unterschieden werden muss, kann operator[] ein Proxy zurückgeben. Siehe z. B. std::bitset::operator[].
|
operator[] kann nur einen Index aufnehmen. Um multidimensionale Array-Zugriffssemantik zu bieten, z. B. um einen 3D-Array-Zugriff a[i][j][k] = x; zu implementieren, muss operator[] eine Referenz auf eine 2D-Ebene zurückgeben, die ihren eigenen operator[] haben muss, der eine Referenz auf eine 1D-Zeile zurückgibt, die operator[] haben muss, der eine Referenz auf das Element zurückgibt. Um diese Komplexität zu vermeiden, entscheiden sich einige Bibliotheken stattdessen für die Überladung von operator(), so dass 3D-Zugriffsausdrücke die Fortran-ähnliche Syntax a(i, j, k) = x; haben. |
(bis C++23) |
|
operator[] kann beliebig viele Indizes aufnehmen. Zum Beispiel kann ein operator[] einer 3D-Array-Klasse, deklariert als T& operator[](std::size_t x, std::size_t y, std::size_t z); direkt auf die Elemente zugreifen. Führen Sie diesen Code aus #include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } Ausgabe v[3, 2, 1] = 42 |
(seit C++23) |
[edit] Bitweise arithmetische Operatoren
Benutzerdefinierte Klassen und Aufzählungen, die die Anforderungen an BitmaskType erfüllen, müssen die bitweisen arithmetischen Operatoren operator&, operator|, operator^, operator~, operator&=, operator|= und operator^= überladen und können optional die Schiebeoperatoren operator<< operator>>, operator>>= und operator<<= überladen. Die kanonischen Implementierungen folgen normalerweise dem Muster für binäre arithmetische Operatoren, das oben beschrieben wurde.
[edit] Boolescher Negationsoperator
|
Der Operator operator! wird häufig von benutzerdefinierten Klassen überladen, die für die Verwendung in booleschen Kontexten bestimmt sind. Solche Klassen stellen auch eine benutzerdefinierte Konvertierungsfunktion in den booleschen Typ bereit (siehe std::basic_ios als Beispiel in der Standardbibliothek) und das erwartete Verhalten von operator! ist, den Wert zu liefern, der dem von operator bool entgegengesetzt ist. |
(bis C++11) |
|
Da der eingebaute Operator ! eine kontextbezogene Konvertierung in bool durchführt, können benutzerdefinierte Klassen, die für die Verwendung in booleschen Kontexten bestimmt sind, nur operator bool bereitstellen und müssen operator! nicht überladen. |
(seit C++11) |
[edit] Selten überladene Operatoren
Die folgenden Operatoren werden selten überladen
- Der Adressoperator, operator&. Wenn das unäre & auf ein lvalue vom unvollständigen Typ angewendet wird und der vollständige Typ einen überladenen operator& deklariert, ist es undefiniert, ob der Operator die eingebaute Bedeutung hat oder die Operatorfunktion aufgerufen wird. Da dieser Operator überladen werden kann, verwenden generische Bibliotheken std::addressof, um Adressen von Objekten benutzerdefinierter Typen zu erhalten. Das bekannteste Beispiel für einen kanonischen überladenen operator& ist die Microsoft-Klasse
CComPtrBase. Ein Beispiel für die Verwendung dieses Operators in EDSL findet sich in boost.spirit. - Die booleschen Logikoperatoren, operator&& und operator||. Im Gegensatz zu den eingebauten Versionen können die Überladungen keine Kurzschlussauswertung implementieren.Im Gegensatz zu den eingebauten Versionen sequenzieren sie auch nicht ihren linken Operanden vor dem rechten.(bis C++17) In der Standardbibliothek werden diese Operatoren nur für std::valarray überladen.
- Der Kommaoperator, operator,. Im Gegensatz zur eingebauten Version sequenzieren die Überladungen ihren linken Operanden nicht vor dem rechten.(bis C++17) Da dieser Operator überladen werden kann, verwenden generische Bibliotheken Ausdrücke wie a, void(), b anstelle von a, b, um die Ausführung von Ausdrücken benutzerdefinierter Typen zu sequenzieren. Die Boost-Bibliothek verwendet operator, in boost.assign, boost.spirit und anderen Bibliotheken. Die Datenbankzugriffsbibliothek SOCI überlädt ebenfalls operator,.
- Der Mitgliedszugriff über Zeiger auf Mitglied operator->*. Es gibt keine spezifischen Nachteile beim Überladen dieses Operators, aber er wird in der Praxis selten verwendet. Es wurde vorgeschlagen, dass er Teil einer Smart-Pointer-Schnittstelle sein könnte und wird tatsächlich in dieser Funktion von Akteuren in boost.phoenix verwendet. Er ist in EDSLs wie cpp.react häufiger anzutreffen.
[edit] Anmerkungen
| Feature-Test-Makro | Wert | Std | Feature |
|---|---|---|---|
__cpp_static_call_operator |
202207L |
(C++23) | static operator() |
__cpp_multidimensional_subscript |
202211L |
(C++23) | static operator[] |
[edit] Schlüsselwörter
[edit] Beispiel
#include <iostream> class Fraction { // or C++17's std::gcd constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d); d = d * rhs.d / gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction f1{3, 8}, f2{1, 2}, f3{10, 2}; std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; static_assert(f3 == f2 * 10); }
Ausgabe
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
[bearbeiten] Defect reports
Die folgenden Verhaltensändernden Fehlerberichte wurden rückwirkend auf zuvor veröffentlichte C++-Standards angewendet.
| DR | angewendet auf | Verhalten wie veröffentlicht | Korrigiertes Verhalten |
|---|---|---|---|
| CWG 1481 | C++98 | der nicht-Member-Präfix-Inkrement-Operator konnte nur einen Parameter vom Klassentyp, Enumerationstyp oder einem Referenztyp auf solche Typen haben |
keine Typanforderung |
| CWG 2931 | C++23 | explizite Objekt-Member-Operatorfunktionen konnten keinen Parameter haben vom Klassentyp, Enumerationstyp oder einem Referenztyp auf solche Typen haben |
verboten |
[bearbeiten] Siehe auch
| Häufige Operatoren | ||||||
|---|---|---|---|---|---|---|
| Zuweisung | Inkrement Dekrement |
Arithmetik | Logisch | Vergleich | Member Zugriff |
Sonstiges |
|
a = b |
++a |
+a |
!a |
a == b |
a[...] |
Funktionsaufruf a(...) |
| Komma a, b | ||||||
| Ternär a ? b : c | ||||||
| Spezielle Operatoren | ||||||
|
static_cast konvertiert einen Typ in einen anderen verwandten Typ | ||||||
[bearbeiten] Externe Links
|