it-swarm.com.de

Was sind die Grundregeln und Redewendungen für das Überladen von Operatoren?

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.)

2035
sbi

Gemeinsame Operatoren zu überladen

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.

Aufgabenverwalter

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;
}

Bitshift-Operatoren (für Stream-E/A verwendet)

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.

Funktionsaufruf Operator

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.

Vergleichsoperatoren

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.

Rechenzeichen

Unäre arithmetische Operatoren

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.

Binäre arithmetische Operatoren

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.

Array-Subskription

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;
  // ...
};

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 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

991
sbi

Die drei Grundregeln für das Überladen von Operatoren in C++

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.

  1. 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.

  2. 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.

  3. 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 .

473
sbi

Die allgemeine Syntax für das Überladen von Operatoren in C++

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: .::sizeoftypeid.* und der einzige ternäre Operator in C++, ?:

Unter den Operatoren, die in C++ überladen werden können, sind folgende:

  • arithmetische Operatoren: +-*/% und +=-=*=/=%= (alle binären Infixe); +- (unäres Präfix); ++-- (unäres Präfix und Postfix)
  • bitmanipulation: &|^<<>> und &=|=^=<<=>>= (alle binären Infixe); ~ (unäres Präfix)
  • boolesche Algebra: ==!=<><=>=||&& (alle binären Infixe); ! (unäres Präfix)
  • speicherverwaltung: newnew[]deletedelete[]
  • implizite Konvertierungsoperatoren
  • verschiedenes: =[]->->*, (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++ .

252
sbi

Die Entscheidung zwischen Mitglied und Nichtmitglied

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:

  1. Wenn es sich um einen unären Operator handelt, implementieren Sie ihn als member Funktion.
  2. Wenn ein binärer Operator beide Operanden gleich behandelt (sie bleiben unverändert), implementieren Sie diesen Operator als Nichtmitglied Funktion.
  3. Wenn ein binärer Operator nicht beide Operanden behandelt gleichermaßen (normalerweise ändert es seinen linken Operanden), es könnte nützlich sein, es zu einem = zu machen member Funktion des linken Operandentyps, wenn auf die privaten Teile des Operanden zugegriffen werden soll.

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 .

233
sbi

Konvertierungsoperatoren (auch als benutzerdefinierte Konvertierungen bezeichnet)

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.

Implizite Konvertierungsoperatoren (C++ 98/C++ 03 und C++ 11)

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.

Explizite Konvertierungsoperatoren (C++ 11)

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 .

159
JKor

Überladen von 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.

Grundlagen

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.

Placement 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.

Klassenspezifisch neu und löschen

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.

Global neu und löschen

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.

144
sbi

Warum kann die Funktion operator<< zum Streamen von Objekten zu std::cout oder zu einer Datei keine Mitgliedsfunktion sein?

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.

39
R Sahu