Gemeinsame Bediener zu überlasten
Der größte Teil der Arbeit bei Überlastungsbetreibern ist der Kesselplattencode. Das ist kein Wunder, da Operatoren lediglich syntaktischer Zucker sind und ihre eigentliche Arbeit durch einfache Funktionen erledigt werden könnte (und häufig an diese weitergeleitet wird). Es ist jedoch wichtig, dass Sie diesen Kesselplattencode richtig verstehen. Wenn Sie fehlschlagen, wird entweder der Code Ihres Bedieners nicht kompiliert oder der Code Ihrer Benutzer wird nicht kompiliert, oder der Code Ihrer Benutzer verhält sich überraschend.
Aufgabenverwalter
Über die Aufgabe gibt es viel zu sagen. Das meiste davon wurde jedoch bereits in den berühmten Copy-And-Swap-FAQ von GMan erwähnt. Daher überspringe ich das meiste hier und liste nur den perfekten Zuweisungsoperator als Referenz auf:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Bitshift-Operatoren (für Stream-E / A verwendet)
Die Bitverschiebungsoperatoren <<
und >>
, obwohl sie immer noch in der Hardware-Schnittstelle für die von C geerbten Bitmanipulationsfunktionen verwendet werden, sind in den meisten Anwendungen als überlastete Stream-Eingabe- und Ausgabeoperatoren häufiger geworden. Informationen zum Überladen von Anleitungen als Bitmanipulationsoperatoren finden Sie im folgenden Abschnitt über binäre arithmetische Operatoren. Fahren Sie fort, um Ihr eigenes benutzerdefiniertes Format und Ihre Parsing-Logik zu implementieren, wenn Ihr Objekt mit iostreams verwendet wird.
Die Stream-Operatoren gehören zu den am häufigsten überladenen Operatoren binäre Infix-Operatoren, für die die Syntax keine Einschränkung angibt, ob sie Mitglieder oder Nichtmitglieder sein sollen. Da sie ihr linkes Argument ändern (sie ändern den Status des Streams), sollten sie gemäß den Faustregeln als Mitglieder des Typs ihres linken Operanden implementiert werden. Ihre linken Operanden sind jedoch Streams aus der Standardbibliothek, und während die meisten von der Standardbibliothek definierten Stream-Ausgabe- und Eingabeoperatoren tatsächlich als Mitglieder der Stream-Klassen definiert sind, werden Sie beim Implementieren von Ausgabe- und Eingabeoperationen für Ihre eigenen Typen Die Stream-Typen der Standardbibliothek können nicht geändert werden. Aus diesem Grund müssen Sie diese Operatoren für Ihre eigenen Typen als Nichtmitgliedsfunktionen implementieren. Die kanonischen Formen der beiden sind folgende:
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( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Bei der Implementierung operator>>
ist das manuelle Festlegen des Stream-Status nur erforderlich, wenn das Lesen selbst erfolgreich war. Das Ergebnis entspricht jedoch nicht den Erwartungen.
Funktionsaufruf-Operator
Der Funktionsaufruf Operator, verwendete Funktionsobjekte zu erstellen, die auch als functors bekannt ist , muss als definiert wird Mitglied Funktion, so dass es immer das implizite hat this
Argument der Member - Funktionen. Davon abgesehen kann es überladen werden, eine beliebige Anzahl zusätzlicher Argumente zu verwenden, einschließlich Null.
Hier ist ein Beispiel für die Syntax:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Verwendungszweck:
foo f;
int a = f("hello");
In der gesamten C ++ - Standardbibliothek werden Funktionsobjekte immer kopiert. Ihre eigenen Funktionsobjekte sollten daher billig zu kopieren sein. Wenn ein Funktionsobjekt unbedingt Daten verwenden muss, deren Kopieren teuer ist, ist es besser, diese Daten an anderer Stelle zu speichern und das Funktionsobjekt darauf verweisen zu lassen.
Vergleichsoperatoren
Die binären Infix-Vergleichsoperatoren sollten gemäß den Faustregeln als Nichtmitgliedsfunktionen 1 implementiert werden . Die unäre Präfixnegation !
sollte (nach denselben Regeln) als Mitgliedsfunktion implementiert werden. (aber es ist normalerweise keine gute Idee, es zu überladen.)
Die Algorithmen (z. B. std::sort()
) und Typen (z. B. ) der Standardbibliothek std::map
erwarten immer nur, dass operator<
sie vorhanden sind. Die Benutzer Ihres Typs erwarten jedoch, dass auch alle anderen Operatoren vorhanden sind. Wenn Sie also definieren operator<
, müssen Sie die dritte Grundregel der Operatorüberladung befolgen und auch alle anderen booleschen Vergleichsoperatoren definieren. Der kanonische Weg, sie umzusetzen, ist folgender:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Das Wichtigste dabei ist, dass nur zwei dieser Operatoren tatsächlich etwas tun, die anderen leiten ihre Argumente nur an einen dieser beiden weiter, um die eigentliche Arbeit zu erledigen.
Die Syntax zum Überladen der verbleibenden binären booleschen Operatoren ( ||
, &&
) folgt den Regeln der Vergleichsoperatoren. Es ist jedoch sehr unwahrscheinlich, dass Sie einen vernünftigen Anwendungsfall für diese 2 finden .
1 Wie bei allen Faustregeln kann es manchmal auch Gründe geben, diese zu brechen. Wenn ja, vergessen Sie nicht, dass der linke Operand der binären Vergleichsoperatoren, der für Elementfunktionen sein wird *this
, auch sein muss const
. Ein als Elementfunktion implementierter Vergleichsoperator müsste also diese Signatur haben:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Beachten Sie das const
am Ende.)
2 Es sollte angemerkt werden , dass die integrierte Version von ||
und &&
Verwendung Verknüpfung Semantik. Während die benutzerdefinierten (weil sie syntaktischer Zucker für Methodenaufrufe sind) keine Verknüpfungssemantik verwenden. Der Benutzer erwartet von diesen Operatoren eine Verknüpfungssemantik, und ihr Code kann davon abhängen. Daher wird dringend empfohlen, sie NIEMALS zu definieren.
Rechenzeichen
Unäre arithmetische Operatoren
Die unären Inkrementierungs- und Dekrementierungsoperatoren sind sowohl in der Präfix- als auch in der Postfix-Variante erhältlich. Um voneinander zu unterscheiden, verwenden die Postfix-Varianten ein zusätzliches Dummy-Int-Argument. Wenn Sie das Inkrementieren oder Dekrementieren überladen, müssen Sie immer sowohl die Präfix- als auch die Postfix-Version implementieren. Hier ist die kanonische Implementierung von Inkrement, Dekrement folgt den gleichen Regeln:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Beachten Sie, dass die Postfix-Variante in Bezug auf das Präfix implementiert ist. Beachten Sie auch, dass Postfix eine zusätzliche Kopie erstellt. 2
Das Überladen von unären Minus- und Pluswerten ist nicht sehr häufig und wird wahrscheinlich am besten vermieden. Bei Bedarf sollten sie wahrscheinlich als Mitgliedsfunktionen überladen werden.
2 Beachten Sie auch, dass die Postfix-Variante mehr Arbeit leistet und daher weniger effizient zu verwenden ist als die Präfix-Variante. Dies ist ein guter Grund, das Präfixinkrement im Allgemeinen dem Postfixinkrement vorzuziehen. Während Compiler normalerweise die zusätzliche Arbeit des Postfix-Inkrements für integrierte Typen optimieren können, können sie dies möglicherweise nicht für benutzerdefinierte Typen tun (was so harmlos aussehen könnte wie ein Listeniterator). Sobald Sie sich daran gewöhnt haben i++
, wird es sehr schwierig, sich daran zu erinnern, ++i
stattdessen etwas zu tun, wenn i
es sich nicht um einen integrierten Typ handelt (und Sie müssten den Code ändern, wenn Sie einen Typ ändern). Daher ist es besser, sich immer daran zu gewöhnen Verwenden des Präfixinkrements, sofern nicht ausdrücklich ein Postfix benötigt wird.
Binäre arithmetische Operatoren
Vergessen Sie bei den binären arithmetischen Operatoren nicht, die Überladung des dritten Grundregeloperators zu beachten : Wenn Sie angeben +
, geben Sie auch an +=
, wenn Sie angeben -
, lassen Sie nicht weg -=
usw. Andrew Koenig soll als erster festgestellt haben, dass die zusammengesetzte Zuordnung Operatoren können als Basis für ihre nicht zusammengesetzten Gegenstücke verwendet werden. Das heißt, der Operator +
wird in Bezug auf implementiert +=
, -
wird in Bezug auf implementiert -=
usw.
Gemäß unseren Faustregeln sollten +
seine Begleiter Nichtmitglieder sein, während ihre Gegenstücke für die zusammengesetzte Zuweisung ( +=
usw.), die ihr linkes Argument ändern, Mitglied sein sollten. Hier ist der beispielhafte Code für +=
und +
; Die anderen binären arithmetischen Operatoren sollten auf die gleiche Weise implementiert werden:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
Gibt das Ergebnis pro Referenz zurück, während operator+
eine Kopie des Ergebnisses zurückgegeben wird. Natürlich ist das Zurücksenden einer Referenz normalerweise effizienter als das Zurücksenden einer Kopie, aber im Fall von operator+
führt kein Weg an dem Kopieren vorbei. Wenn Sie schreiben a + b
, erwarten Sie, dass das Ergebnis ein neuer Wert ist, weshalb operator+
ein neuer Wert zurückgegeben werden muss. 3
Beachten Sie auch, dass operator+
der linke Operand durch Kopieren und nicht durch Konstantenreferenz verwendet wird. Der Grund dafür ist der gleiche wie der Grund für operator=
die Argumentation pro Kopie.
Die Bitmanipulationsoperatoren ~
&
|
^
<<
>>
sollten auf die gleiche Weise wie die arithmetischen Operatoren implementiert werden. Es gibt jedoch (mit Ausnahme der Überladung <<
sowie der >>
Ausgabe und Eingabe) nur sehr wenige vernünftige Anwendungsfälle für die Überladung dieser.
3 Die Lehre daraus ist wiederum, dass sie a += b
im Allgemeinen effizienter ist als a + b
und wenn möglich bevorzugt werden sollte.
Array-Subskription
Der Array-Indexoperator ist ein binärer Operator, der als Klassenmitglied implementiert werden muss. Es wird für containerähnliche Typen verwendet, die den Zugriff auf ihre Datenelemente über einen Schlüssel ermöglichen. Die kanonische Form der Bereitstellung dieser ist folgende:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Sofern Sie nicht möchten, dass Benutzer Ihrer Klasse die von zurückgegebenen Datenelemente ändern können operator[]
(in diesem Fall können Sie die nicht konstante Variante weglassen), sollten Sie immer beide Varianten des Operators angeben.
Wenn bekannt ist, dass value_type auf einen integrierten Typ verweist, sollte die const-Variante des Operators besser eine Kopie anstelle einer const-Referenz zurückgeben:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operatoren für zeigerähnliche Typen
Um Ihre eigenen Iteratoren oder intelligenten Zeiger zu definieren, müssen Sie den unären Präfix-Dereferenzierungsoperator *
und den Zugriffsoperator für binäre Infixzeiger überladen ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Beachten Sie, dass auch diese fast immer sowohl eine const- als auch eine non-const-Version benötigen. Wenn der ->
Operator value_type
vom Typ class
(oder struct
oder union
) ist, wird ein anderer operator->()
rekursiv aufgerufen, bis a operator->()
einen Wert vom Typ Nichtklasse zurückgibt.
Die unäre Adresse des Operators sollte niemals überladen werden.
Für operator->*()
sieht diese Frage . Es wird selten benutzt und ist daher selten überlastet. Tatsächlich überladen es selbst Iteratoren nicht.
Fahren Sie mit den Konvertierungsoperatoren fort