Hinweis: Die Antworten wurden in einer bestimmten Reihenfolge angegeben. Da jedoch viele Benutzer die Antworten nicht nach der angegebenen Zeit, sondern nach Stimmen sortieren, finden Sie hier ein Index der Antworten in der Reihenfolge, in der sie am sinnvollsten sind:
(Hinweis: Dies ist ein Eintrag in C++ - FAQ von Stack Overflow . Wenn Sie die Idee kritisieren möchten, in diesem Formular ein FAQ bereitzustellen Dann ist das Posting auf Meta, das all dies gestartet hat der richtige Ort dafür. Antworten auf diese Frage werden im C++ - Chatroom überwacht, wo die FAQ Idee begann an erster Stelle, daher wird Ihre Antwort sehr wahrscheinlich von denjenigen gelesen, die auf die Idee gekommen sind.)
Die meiste Arbeit beim Überladen von Bedienern besteht aus Kesselschildern. Das ist kein Wunder, da Operatoren nur syntaktischer Zucker sind, könnte ihre eigentliche Arbeit durch einfache Funktionen erledigt werden (und wird oft an diese weitergeleitet). Es ist jedoch wichtig, dass Sie diesen Kesselschildcode richtig verstehen. Wenn Sie fehlschlagen, wird entweder der Code Ihres Betreibers nicht kompiliert, oder der Code Ihrer Benutzer wird nicht kompiliert, oder der Code Ihrer Benutzer verhält sich überraschend.
Es gibt viel über die Zuweisung zu sagen. Das meiste davon wurde jedoch bereits in GMans berühmten FAQs zum Kopieren und Austauschen gesagt, daher überspringe ich das meiste hier und liste nur den perfekten Zuweisungsoperator als Referenz auf:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Die Bitverschiebungsoperatoren <<
und >>
werden zwar immer noch in der Hardwareschnittstelle für die Bitmanipulationsfunktionen verwendet, die sie von C erben. Informationen zum Überladen von Anweisungen als Bitmanipulationsoperatoren finden Sie im folgenden Abschnitt über binäre arithmetische Operatoren. Fahren Sie mit der Implementierung Ihres eigenen benutzerdefinierten Formats und der Parsing-Logik fort, wenn Ihr Objekt mit iostreams verwendet wird.
Die Stream-Operatoren gehören zu den am häufigsten überladenen Operatoren und sind binäre Infix-Operatoren, für die in der Syntax keine Einschränkung festgelegt ist, 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 Member 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, implementieren Sie 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:
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 von operator>>
muss der Status des Streams nur manuell festgelegt werden, wenn das Lesen selbst erfolgreich war. Das Ergebnis ist jedoch nicht das, was erwartet wird.
Der Funktionsaufrufoperator, der zum Erstellen von Funktionsobjekten verwendet wird, auch als Funktoren bezeichnet, muss als member function definiert werden, damit er immer den impliziten this
enthält. Argument der Mitgliedsfunktionen. Ansonsten kann es überladen sein, eine beliebige Anzahl zusätzlicher Argumente aufzunehmen, 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. Eigene Funktionsobjekte sollten daher günstig zu kopieren sein. Wenn ein Funktionsobjekt unbedingt Daten verwenden muss, deren Kopieren teuer ist, ist es besser, diese Daten an einer anderen Stelle zu speichern und das Funktionsobjekt darauf verweisen zu lassen.
Die Vergleichsoperatoren für binäre Infixe sollten gemäß den Faustregeln als Nicht-Member-Funktionen implementiert werden1. Die Negation des unären Präfixes !
sollte (nach den gleichen Regeln) als Member-Funktion implementiert werden. (Aber es ist normalerweise keine gute Idee, es zu überladen.)
Die Algorithmen (z. B. std::sort()
) und Typen der Standardbibliothek (z. B. std::map
) erwarten immer nur, dass operator<
vorhanden ist. Die Benutzer Ihres Typs erwarten jedoch, dass auch alle anderen Operatoren vorhanden sind . Wenn Sie also operator<
definieren, müssen Sie die dritte Grundregel für das Überladen von Operatoren befolgen Definieren Sie alle anderen booleschen Vergleichsoperatoren. Der kanonische Weg, sie zu implementieren, 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);}
Hierbei ist zu beachten, dass nur zwei dieser Operatoren tatsächlich etwas tun. Die anderen leiten ihre Argumente lediglich an einen dieser beiden Operatoren 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 finden2.
1Wie 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, die für Elementfunktionen *this
sein werden, auch const
sein muss. Ein Vergleichsoperator, der als Member-Funktion implementiert ist, muss also die folgende Signatur haben:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Beachten Sie den const
am Ende.)
2Es sollte beachtet werden, dass die integrierte Version von ||
und &&
eine Verknüpfungssemantik verwendet. Während die benutzerdefinierten (weil sie syntaktischer Zucker für Methodenaufrufe sind) keine Abkürzungssemantik verwenden. Der Benutzer erwartet von diesen Operatoren eine Shortcut-Semantik, und ihr Code kann davon abhängen. Daher wird dringend empfohlen, sie NIEMALS zu definieren.
Die unären Inkrement- und Dekrement-Operatoren sind sowohl als Präfix- als auch als Postfix-Operatoren verfügbar. Um voneinander zu unterscheiden, verwenden die Postfix-Varianten ein zusätzliches Dummy-Int-Argument. Stellen Sie bei einer Überladung von Inkrement oder Dekrement sicher, dass Sie immer sowohl Präfix- als auch Postfix-Versionen 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 als Präfix implementiert ist. Beachten Sie auch, dass Postfix eine zusätzliche Kopie erstellt.2
Das Überladen von unärem Minus und Plus ist nicht sehr verbreitet und wird wahrscheinlich am besten vermieden. Bei Bedarf sollten sie wahrscheinlich als Member-Funktionen überladen werden.
2Beachten Sie auch, dass die Postfix-Variante mehr Arbeit leistet und daher weniger effizient ist als die Präfix-Variante. Dies ist ein guter Grund, das Präfixinkrement generell dem Postfixinkrement vorzuziehen. Während Compiler normalerweise die zusätzliche Arbeit des Postfix-Inkrements für integrierte Typen optimieren können, sind sie möglicherweise nicht in der Lage, dasselbe für benutzerdefinierte Typen zu tun (was so harmlos sein könnte wie ein Listeniterator). Sobald Sie sich an i++
gewöhnt haben, wird es sehr schwer, sich daran zu erinnern, ++i
zu tun, wenn i
keinen eingebauten Typ hat (und Sie den Code ändern müssen, wenn Sie einen Typ ändern) Gewohnheit, immer ein Präfixinkrement zu verwenden, es sei denn, Postfix wird ausdrücklich benötigt.
Vergessen Sie für die binären arithmetischen Operatoren nicht, die Überladung des dritten Grundregeloperators zu beachten: Wenn Sie +
angeben, auch +=
angeben, wenn Sie -
angeben, nicht -=
auslassen, usw. Andrew Koenig soll der erste gewesen sein, der dies beobachtet hat dass die zusammengesetzten Zuweisungsoperatoren als Basis für ihre nicht zusammengesetzten Gegenstücke verwendet werden können. Das heißt, der Operator +
wird in Bezug auf +=
implementiert, -
wird in Bezug auf -=
implementiert usw.
Gemäß unseren Faustregeln sollten +
und seine Gefährten keine Mitglieder sein, während ihre Gegenstücke für zusammengesetzte Zuweisungen (+=
usw.), die ihr linkes Argument ändern, Mitglied sein sollten. Hier ist der Beispielcode 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ückgibt. Das Zurücksenden einer Referenz ist normalerweise effizienter als das Zurücksenden einer Kopie, aber im Fall von operator+
führt kein Weg an dem Kopieren vorbei. Wenn Sie a + b
schreiben, erwarten Sie, dass das Ergebnis ein neuer Wert ist. Aus diesem Grund muss operator+
einen neuen Wert zurückgeben.3 Beachten Sie auch, dass operator+
seinen linken Operanden durch copy und nicht durch const reference erhält. Der Grund dafür ist der gleiche wie der Grund, warum operator=
sein Argument pro Kopie verwendet.
Die Bitmanipulationsoperatoren ~
&
|
^
<<
>>
sollten genauso implementiert werden wie die arithmetischen Operatoren. Es gibt jedoch nur sehr wenige sinnvolle Anwendungsfälle, um diese zu überladen (mit Ausnahme des Überladens von <<
und >>
für Ausgabe und Eingabe).
3Die Lehre daraus ist wiederum, dass a += b
im Allgemeinen effizienter ist als a + b
und nach Möglichkeit bevorzugt werden sollte.
Der Array-Indexoperator ist ein Binäroperator, 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, diese bereitzustellen, 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 operator[]
zurückgegebenen Datenelemente ändern können (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 eingebauten Typ verweist, sollte die const-Variante des Operators eine Kopie anstelle einer const-Referenz zurückgeben:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Um Ihre eigenen Iteratoren oder intelligenten Zeiger zu definieren, müssen Sie den unären Präfix-Dereferenzierungsoperator *
und den binären Infix-Zeiger-Mitgliedszugriffsoperator ->
ü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 für den Operator ->
value_type
vom Typ class
(oder struct
oder union
) ist, wird ein anderer operator->()
rekursiv aufgerufen, bis ein operator->()
einen Wert vom Typ ohne Klasse zurückgibt.
Die unäre Adresse des Operators sollte niemals überladen werden.
Für operator->*()
siehe diese Frage . Es wird selten verwendet und ist daher selten überlastet. Selbst Iteratoren überlasten sie nicht.
Weiter zu Konvertierungsoperatoren
Wenn es um das Überladen von Operatoren in C++ geht, gibt es drei Grundregeln, die Sie befolgen sollten . Wie bei all diesen Regeln gibt es tatsächlich Ausnahmen. Manchmal sind die Leute von ihnen abgewichen und das Ergebnis war kein schlechter Code, aber solche positiven Abweichungen sind selten. Zumindest waren 99 von 100 Abweichungen, die ich gesehen habe, ungerechtfertigt. Es könnte jedoch genauso gut 999 von 1000 gewesen sein. Halten Sie sich also besser an die folgenden Regeln.
Wenn die Bedeutung eines Operators nicht eindeutig und unbestritten ist, sollte er nicht überladen werden. Geben Sie stattdessen eine Funktion mit einem ausgewählten Namen an.
Grundsätzlich lautet die wichtigste Regel zur Überlastung von Bedienern im Kern: Tun Sie es nicht . Das mag seltsam erscheinen, denn es gibt eine Menge zu wissen, was das Überladen von Operatoren angeht, und deshalb befassen sich viele Artikel, Buchkapitel und andere Texte mit all dem. Trotz dieser scheinbar offensichtlichen Beweise gibt es nur überraschend wenige Fälle, in denen eine Überladung des Operators angebracht ist . Der Grund ist, dass es tatsächlich schwierig ist, die Semantik hinter der Anwendung eines Operators zu verstehen, es sei denn, die Verwendung des Operators in der Anwendungsdomäne ist bekannt und unbestritten. Dies ist entgegen der landläufigen Meinung kaum der Fall.
Halten Sie sich immer an die bekannte Semantik des Operators.
C++ schränkt die Semantik überladener Operatoren nicht ein. Ihr Compiler akzeptiert gerne Code, der den binären Operator +
implementiert, um von seinem rechten Operanden zu subtrahieren. Die Benutzer eines solchen Operators würden jedoch niemals den Ausdruck a + b
vermuten, um a
von b
zu subtrahieren. Dies setzt natürlich voraus, dass die Semantik des Operators in der Anwendungsdomäne unbestritten ist.
Geben Sie immer alle zugehörigen Operationen an.
Operatoren sind miteinander und mit anderen Operationen verwandt. Wenn Ihr Typ a + b
unterstützt, können Benutzer auch a += b
aufrufen. Wenn es das Präfixinkrement ++a
unterstützt, wird erwartet, dass a++
ebenfalls funktioniert. Wenn sie prüfen können, ob a < b
, werden sie mit Sicherheit auch erwarten können, ob a > b
. Wenn sie Ihren Typ kopieren und konstruieren können, erwarten sie, dass die Zuweisung ebenfalls funktioniert.
Weiter zu Die Entscheidung zwischen Mitglied und Nichtmitglied .
Sie können die Bedeutung von Operatoren für integrierte Typen in C++ nicht ändern. Operatoren können nur für benutzerdefinierte Typen überladen werden1. Das heißt, mindestens einer der Operanden muss von einem benutzerdefinierten Typ sein. Wie bei anderen überladenen Funktionen können Bediener für einen bestimmten Parametersatz nur einmal überladen werden.
Nicht alle Operatoren können in C++ überladen werden. Zu den Operatoren, die nicht überladen werden können, gehören: .
::
sizeof
typeid
.*
und der einzige ternäre Operator in C++, ?:
Unter den Operatoren, die in C++ überladen werden können, sind folgende:
+
-
*
/
%
und +=
-=
*=
/=
%=
(alle binären Infixe); +
-
(unäres Präfix); ++
--
(unäres Präfix und Postfix)&
|
^
<<
>>
und &=
|=
^=
<<=
>>=
(alle binären Infixe); ~
(unäres Präfix)==
!=
<
>
<=
>=
||
&&
(alle binären Infixe); !
(unäres Präfix)new
new[]
delete
delete[]
=
[]
->
->*
,
(alles binäres Infix); *
&
(alle unären Präfixe) ()
(Funktionsaufruf, n-äriges Infix)Die Tatsache, dass Sie all diese überladen können , bedeutet jedoch nicht, dass Sie dies tun sollten . Beachten Sie die Grundregeln für das Überladen von Operatoren.
In C++ werden Operatoren in Form von - Funktionen mit speziellen Namen überladen. Wie bei anderen Funktionen können überladene Operatoren im Allgemeinen entweder als Elementfunktion des Typs ihres linken Operanden oder als Nichtmitglieder-Funktionen . Ob Sie eines auswählen oder festlegen können, hängt von mehreren Kriterien ab.2 Ein unärer Operator @
3, angewendet auf ein Objekt x, wird entweder als [email protected](x)
oder als [email protected]()
aufgerufen. Ein binärer Infix-Operator @
, der auf die Objekte x
und y
angewendet wird, heißt entweder [email protected](x,y)
oder [email protected](y)
.4
Operatoren, die als Nicht-Member-Funktionen implementiert sind, sind manchmal mit dem Typ ihres Operanden befreundet.
1Der Begriff "benutzerdefiniert" kann leicht irreführend sein. C++ unterscheidet zwischen integrierten und benutzerdefinierten Typen. Zu ersteren gehören zum Beispiel int, char und double; Zu letzteren gehören alle Struktur-, Klassen-, Vereinigungs- und Aufzählungstypen, einschließlich derer aus der Standardbibliothek, auch wenn sie als solche nicht von Benutzern definiert werden.
2Dies wird in einem späteren Teil dieser FAQ behandelt.
3Der @
ist kein gültiger Operator in C++, weshalb ich ihn als Platzhalter verwende.
4Der einzige ternäre Operator in C++ kann nicht überladen werden, und der einzige n-fache Operator muss immer als Member-Funktion implementiert werden.
Fahren Sie fort mit Die drei Grundregeln für das Überladen von Operatoren in C++ .
Die binären Operatoren _=
_ (Zuweisung), _[]
_ (Array-Abonnement), _->
_ (Mitgliederzugriff) sowie der Operator n-ary _()
_ (Funktionsaufruf), muss immer als member functions implementiert werden, da die Syntax der Sprache dies erfordert.
Andere Operatoren können entweder als Mitglieder oder als Nichtmitglieder implementiert werden. Einige von ihnen müssen jedoch normalerweise als Nicht-Member-Funktionen implementiert werden, da ihr linker Operand von Ihnen nicht geändert werden kann. Die bekanntesten davon sind die Eingabe- und Ausgabeoperatoren _<<
_ und _>>
_, deren linke Operanden Stream-Klassen aus der Standardbibliothek sind, die Sie nicht ändern können.
Für alle Operatoren, bei denen Sie wählen müssen, ob Sie sie als Memberfunktion oder als Nicht-Memberfunktion implementieren möchten, verwenden Sie die folgenden Faustregeln zu entscheiden:
Natürlich gibt es wie bei allen Faustregeln Ausnahmen. Wenn Sie einen Typ haben
_enum Month {Jan, Feb, ..., Nov, Dec}
_
wenn Sie die Inkrement- und Dekrement-Operatoren dafür überladen möchten, können Sie dies nicht als Member-Funktionen ausführen, da Enum-Typen in C++ keine Member-Funktionen haben können. Sie müssen es also als freie Funktion überladen. Und operator<()
für eine Klassenvorlage, die in einer Klassenvorlage verschachtelt ist, ist viel einfacher zu schreiben und zu lesen, wenn sie als Elementfunktion in der Klassendefinition eingebunden ist. Aber das sind in der Tat seltene Ausnahmen.
(Wenn Sie jedoch if eine Ausnahme machen, vergessen Sie nicht das Problem const
- für den Operanden, der für Elementfunktionen zum impliziten Argument this
wird. Wenn der Operator als ein Eine Nicht-Member-Funktion würde das am weitesten links stehende Argument als const
-Referenz verwenden. Derselbe Operator wie eine Member-Funktion muss am Ende eine const
-Referenz haben, um _*this
_ zu einer const
-Referenz zu machen.)
Weiter zu Gemeinsame Operatoren zum Überladen .
In C++ können Sie Konvertierungsoperatoren erstellen, die es dem Compiler ermöglichen, zwischen Ihren Typen und anderen definierten Typen zu konvertieren. Es gibt zwei Arten von Konvertierungsoperatoren: implizite und explizite.
Ein impliziter Konvertierungsoperator ermöglicht es dem Compiler, den Wert eines benutzerdefinierten Typs implizit (wie die Konvertierung zwischen int
und long
) in einen anderen Typ zu konvertieren.
Das Folgende ist eine einfache Klasse mit einem impliziten Konvertierungsoperator:
class my_string {
public:
operator const char*() const {return data_;} // This is the conversion operator
private:
const char* data_;
};
Implizite Konvertierungsoperatoren sind wie Konstruktoren mit einem Argument benutzerdefinierte Konvertierungen. Compiler gewähren eine benutzerdefinierte Konvertierung, wenn sie versuchen, einen Aufruf einer überladenen Funktion zuzuordnen.
void f(const char*);
my_string str;
f(str); // same as f( str.operator const char*() )
Zunächst scheint dies sehr hilfreich zu sein, aber das Problem dabei ist, dass die implizite Konvertierung auch dann einsetzt, wenn dies nicht erwartet wird. Im folgenden Code wird void f(const char*)
aufgerufen, weil my_string()
kein lvalue ist, daher stimmt das erste nicht überein:
void f(my_string&);
void f(const char*);
f(my_string());
Anfänger verstehen das leicht falsch und selbst erfahrene C++ - Programmierer sind manchmal überrascht, weil der Compiler eine Überlastung auswählt, die sie nicht vermutet haben. Diese Probleme können durch explizite Konvertierungsoperatoren gemindert werden.
Im Gegensatz zu impliziten Konvertierungsoperatoren werden explizite Konvertierungsoperatoren niemals aktiviert, wenn Sie dies nicht erwarten. Das Folgende ist eine einfache Klasse mit einem expliziten Konvertierungsoperator:
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};
Beachten Sie die explicit
. Wenn Sie nun versuchen, den unerwarteten Code von den impliziten Konvertierungsoperatoren auszuführen, wird ein Compilerfehler angezeigt:
prog.cpp: In der Funktion 'int main ()': prog.cpp: 15: 18: Fehler: Keine passende Funktion für den Aufruf von 'f (my_string)' prog.cpp: 15: 18: note: Bewerber sind: prog.cpp: 11: 10: note: void f (my_string &) prog.cpp: 11: 10: note: no known Konvertierung für Argument 1 von 'my_string' in 'my_string &' prog.cpp: 12: 10: note: void f (const char *) prog.cpp: 12: 10: note: no bekannte Konvertierung für Argument 1 von 'my_string' nach 'const char *'
Um den expliziten Besetzungsoperator aufzurufen, müssen Sie static_cast
, eine Besetzung im C-Stil oder eine Besetzung im Konstruktorstil (d. H. T(value)
) verwenden.
Es gibt jedoch eine Ausnahme: Der Compiler darf implizit in bool
konvertieren. Darüber hinaus darf der Compiler nach der Konvertierung in bool
keine weitere implizite Konvertierung durchführen (ein Compiler darf maximal 2 implizite Konvertierungen gleichzeitig ausführen, jedoch nur 1 benutzerdefinierte Konvertierung).
Da der Compiler kein "past" bool
konvertiert, müssen explizite Konvertierungsoperatoren jetzt nicht mehr das Safe Bool-Idiom verwenden. Beispielsweise verwendeten intelligente Zeiger vor C++ 11 die Safe Bool-ID, um Konvertierungen in ganzzahlige Typen zu verhindern. In C++ 11 verwenden die intelligenten Zeiger stattdessen einen expliziten Operator, da der Compiler nicht implizit in einen ganzzahligen Typ konvertieren darf, nachdem er einen Typ explizit in bool konvertiert hat.
Fahren Sie fort mit Überladen von new
und delete
.
new
und delete
Hinweis: Dies betrifft nur die Syntax der Überladung von new
und delete
, nicht mit dem Implementierung solcher überladenen Operatoren. Ich denke, dass die Semantik der Überladung new
und delete
ihre eigenen FAQ verdienen, innerhalb des Themas der Operatorüberladung kann ich es nie gerecht machen.
Wenn Sie in C++ einen neuen Ausdruck wie new T(arg)
schreiben, passieren dabei zwei Dinge Der Ausdruck wird ausgewertet: Zuerst wird operator new
aufgerufen, um unformatierten Speicher zu erhalten Der entsprechende Konstruktor von T
wird aufgerufen, um diesen unformatierten Speicher in ein gültiges Objekt umzuwandeln. Wenn Sie ein Objekt löschen, wird zunächst der Destruktor aufgerufen und anschließend der Speicher an _operator delete
_ zurückgegeben.
Mit C++ können Sie beide Vorgänge optimieren: Speicherverwaltung und Konstruktion/Zerstörung des Objekts im zugewiesenen Speicher. Letzteres geschieht, indem Konstruktoren und Destruktoren für eine Klasse geschrieben werden. Die Feinabstimmung der Speicherverwaltung erfolgt durch Schreiben Ihrer eigenen _operator new
_ und _operator delete
_.
Die erste der Grundregeln für das Überladen von Operatoren - Nicht tun - gilt insbesondere für das Überladen von new
und delete
. Fast die einzigen Gründe für eine Überlastung dieser Operatoren sind Leistungsprobleme und Speicherbeschränkungen und in vielen Fällen andere Aktionen, wie Änderungen an den Algorithmen verwendet wird, liefert ein viel höheres Kosten/Gewinn-Verhältnis als der Versuch, die Speicherverwaltung zu optimieren.
Die C++ - Standardbibliothek enthält eine Reihe vordefinierter Operatoren new
und delete
. Die wichtigsten sind:
_void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void*) throw();
_
Die ersten beiden ordnen Speicher für ein Objekt zu bzw. geben Speicher frei, die letzten beiden für ein Array von Objekten. Wenn Sie Ihre eigenen Versionen von diesen bereitstellen, werden sie nicht überladen, sondern ersetzen die aus dem Standardbibliothek.
Wenn Sie _operator new
_ überladen, sollten Sie immer auch den passenden _operator delete
_ überladen, auch wenn Sie nie vorhaben, ihn aufzurufen. Der Grund dafür ist, dass das Laufzeitsystem, wenn ein Konstruktor während der Auswertung eines neuen Ausdrucks ausgelöst wird, den Speicher auf den _operator delete
_ zurücksendet, der dem _operator new
_ entspricht, der aufgerufen wurde, um den Speicher für die Erstellung des zuzuweisen object in. Wenn Sie keinen passenden _operator delete
_ angeben, wird der Standardwert aufgerufen, was fast immer falsch ist.
Wenn Sie new
und delete
überladen, sollten Sie auch die Array-Varianten überladen.
new
In C++ können neue und löschende Operatoren zusätzliche Argumente verwenden.
Mit der sogenannten Platzierung Neu können Sie ein Objekt an einer bestimmten Adresse anlegen, die an folgende Adresse übergeben wird:
_class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
_
Die Standardbibliothek wird mit den entsprechenden Überladungen der Operatoren new und delete geliefert:
_void* operator new(std::size_t,void* p) throw(std::bad_alloc);
void operator delete(void* p,void*) throw();
void* operator new[](std::size_t,void* p) throw(std::bad_alloc);
void operator delete[](void* p,void*) throw();
_
Beachten Sie, dass in dem oben angegebenen Beispielcode für die neue Platzierung _operator delete
_ niemals aufgerufen wird, es sei denn, der Konstruktor von X löst eine Ausnahme aus.
Sie können new
und delete
auch mit anderen Argumenten überladen. Wie beim zusätzlichen Argument für die Platzierung new werden diese Argumente auch nach dem Schlüsselwort new
in Klammern aufgeführt. Lediglich aus historischen Gründen werden solche Varianten oft auch als Plazierung neu bezeichnet, auch wenn ihre Argumente nicht für die Plazierung eines Objekts an einer bestimmten Adresse sprechen.
In den meisten Fällen möchten Sie die Speicherverwaltung optimieren, da Messungen ergeben haben, dass Instanzen einer bestimmten Klasse oder einer Gruppe verwandter Klassen häufig erstellt und zerstört werden und die Standardspeicherverwaltung des Laufzeitsystems darauf abgestimmt ist allgemeine Leistung, geht in diesem konkreten Fall ineffizient um. Um dies zu verbessern, können Sie new überladen und für eine bestimmte Klasse löschen:
_class my_class {
public:
// ...
void* operator new();
void operator delete(void*,std::size_t);
void* operator new[](size_t);
void operator delete[](void*,std::size_t);
// ...
};
_
Überladen verhalten sich also new und delete wie statische Member-Funktionen. Für Objekte von _my_class
_ ist das Argument _std::size_t
_ immer sizeof(my_class)
. Diese Operatoren werden jedoch auch für dynamisch zugewiesene Objekte von abgeleiteten Klassen aufgerufen. In diesem Fall kann dies der Fall sein größer sein als das.
Um das globale Neue und Löschen zu überlasten, ersetzen Sie einfach die vordefinierten Operatoren der Standardbibliothek durch unsere eigenen. Dies muss jedoch selten durchgeführt werden.
Angenommen, Sie haben:
struct Foo
{
int a;
double b;
std::ostream& operator<<(std::ostream& out) const
{
return out << a << " " << b;
}
};
In Anbetracht dessen können Sie nicht verwenden:
Foo f = {10, 20.0};
std::cout << f;
Da operator<<
als Member-Funktion von Foo
überladen ist, muss die LHS des Operators ein Foo
-Objekt sein. Das heißt, Sie müssen Folgendes verwenden:
Foo f = {10, 20.0};
f << std::cout
das ist sehr nicht intuitiv.
Wenn Sie es als Nichtmitgliedsfunktion definieren,
struct Foo
{
int a;
double b;
};
std::ostream& operator<<(std::ostream& out, Foo const& f)
{
return out << f.a << " " << f.b;
}
Sie können verwenden:
Foo f = {10, 20.0};
std::cout << f;
das ist sehr intuitiv.