c++ operator== - Was sind die Grundregeln und Redewendungen für das Überladen von Operatoren?



operator+= overloading (7)

Die allgemeine Syntax des Überladens 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 werden 1 . Das heißt, mindestens einer der Operanden muss vom benutzerdefinierten Typ sein. Wie bei anderen überladenen Funktionen können Operatoren für einen bestimmten Parametersatz nur einmal überladen werden.

Nicht alle Operatoren können in C ++ überladen werden. Unter den Operatoren, die nicht überlastet werden können, sind:. :: typeid .* und der einzige ternäre Operator in C ++, ?:

Zu den Operatoren, die in C ++ überladen werden können, gehören:

  • arithmetische Operatoren: + - * / % und += -= *= /= %= (alle binären Infixe); + - (unäres Präfix); ++ -- (unäres Präfix und Postfix)
  • Bit-Manipulation: & | ^ << >> und &= |= ^= <<= >>= (alle binären Infix); ~ (unäres Präfix)
  • Boolesche Algebra: == != < > <= >= || && (alles binäre Infix); ! (unäres Präfix)
  • Speicherverwaltung: new new[] delete delete[]
  • implizite Konvertierungsoperatoren
  • Miscellany: = [] -> ->* , (alle binären Infix); * & (alles unäre Präfix) () (Funktionsaufruf, n-äre Infix)

Die Tatsache, dass Sie alle ü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 eine Elementfunktion ihres linken Operandentyps oder als Nichtmitgliedsfunktionen implementiert werden. Ob Sie frei entscheiden können oder eines der beiden verwenden möchten, hängt von mehreren Kriterien ab. 2 Ein unärer Operator @ 3 , der auf ein Objekt x angewendet wird, wird entweder als [email protected](x) oder als [email protected]() . Ein binärer Infixoperator @ , angewendet auf die Objekte x und y , wird entweder als [email protected](x,y) oder als [email protected](y) . 4

Operatoren, die als Nicht-Mitgliedsfunktionen implementiert sind, sind manchmal Freunde ihres Operandentyps.

1 Der Begriff "benutzerdefiniert" könnte leicht irreführend sein. C ++ unterscheidet zwischen eingebauten Typen und benutzerdefinierten Typen. Zu den ersten gehören zum Beispiel int, char und double; Zu Letzteren gehören alle Struktur-, Klassen-, Vereinigungs- und Aufzählungstypen, einschließlich der aus der Standardbibliothek, auch wenn sie nicht als solche von Benutzern definiert sind.

2 Dies wird in einem späteren Teil dieser FAQ behandelt.

3 Das @ ist in C ++ kein gültiger Operator, weshalb ich es als Platzhalter verwende.

4 Der einzige ternäre Operator in C ++ kann nicht überladen werden, und der einzige n-stufige Operator muss immer als Elementfunktion implementiert sein.

Fahren Sie mit den drei Grundregeln des Überladens von Operatoren in C ++ fort .

Hinweis: Die Antworten wurden in einer bestimmten Reihenfolge gegeben , aber da viele Benutzer die Antworten nach Abstimmungen sortieren, anstatt nach der Zeit, in der sie gegeben wurden, folgt hier ein Index der Antworten in der Reihenfolge, in der sie am sinnvollsten sind:

(Hinweis: Dies ist ein Eintrag in die C ++ - FAQ von . Wenn Sie die Idee, eine FAQ in diesem Formular bereitzustellen, kritisieren möchten, dann wäre das Posting auf meta, mit dem all dies begonnen wurde , der richtige Ort dafür Diese Frage wird im C ++ - Chatraum überwacht, wo die FAQ-Idee von Anfang an begann, so dass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die die Idee hatten.)


Häufige Operatoren überladen

Die meisten Arbeiten, bei denen Bediener überlastet werden, sind Code für Kesselplatten. Das ist kein Wunder, da Operatoren nur syntaktischer Zucker sind, ihre eigentliche Arbeit könnte durch einfache Funktionen erledigt werden (und wird oft an diese weitergeleitet). Aber es ist wichtig, dass Sie diesen Code für die Kesselplatte erhalten. Wenn Sie scheitern, wird entweder der Code Ihres Operators nicht kompiliert oder der Code Ihres Benutzers wird nicht kompiliert, oder der Code Ihrer Benutzer wird sich überraschend verhalten.

Aufgabenverwalter

Es gibt eine Menge über die Aufgabe zu sagen. Das meiste davon wurde jedoch bereits in GMans berühmten Copy-And-Swap FAQ gesagt, daher werde ich das meiste hier weglassen und nur den perfekten Zuweisungsoperator als Referenz aufführen :

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Bitshift-Operatoren (für Stream I / O)

Die Bitshift-Operatoren << und >> , obwohl sie immer noch in Hardwareschnittstellen für die Bitmanipulationsfunktionen verwendet werden, die sie von C erben, sind in den meisten Anwendungen als überlastete Stream-Eingabe- und Ausgabeoperatoren vorherrschender geworden. Hinweise zum Überladen als Bitmanipulationsoperatoren finden Sie im folgenden Abschnitt Binäre Arithmetikoperatoren. Wenn Sie Ihr eigenes benutzerdefiniertes Format und eine neue Analyselogik implementieren möchten, wenn Ihr Objekt mit Iostreams verwendet wird, fahren Sie fort.

Die Stream-Operatoren, unter den am häufigsten überladenen Operatoren, sind binäre Infix-Operatoren, für die die Syntax keine Einschränkung angibt, ob sie Member oder Nicht-Member 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 obwohl die meisten Stream-Ausgabe- und Eingabeoperatoren, die von der Standardbibliothek definiert werden, tatsächlich als Member der Streamklassen definiert sind, wenn Sie Ausgabe- und Eingabeoperationen für Ihre eigenen Typen implementieren Die Stream-Typen der Standardbibliothek können nicht geändert werden. Deshalb müssen Sie diese Operatoren für Ihre eigenen Typen als Nicht-Member-Funktionen implementieren. Die kanonischen Formen der beiden sind diese:

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

Beim Implementieren von operator>> ist das manuelle Festlegen des Status des Streams nur erforderlich, wenn das Lesen selbst erfolgreich war, aber das Ergebnis ist nicht das, was zu erwarten wäre.

Funktionsaufruf-Operator

Der Funktionsaufrufoperator, der zum Erstellen von Funktionsobjekten verwendet wird, die auch als Funktoren bezeichnet werden, muss als Elementfunktion definiert sein, sodass er immer das implizite Argument der Elementfunktionen enthält. Ansonsten kann es überladen werden, um eine beliebige Anzahl von zusätzlichen Argumenten aufzunehmen, einschließlich Null.

Hier ist ein Beispiel für die Syntax:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

Verwendung:

foo f;
int a = f("hello");

In der C ++ - Standardbibliothek werden Funktionsobjekte immer kopiert. Ihre eigenen Funktionsobjekte sollten daher kostengünstig zu kopieren sein. Wenn ein Funktionsobjekt unbedingt Daten verwenden muss, die teuer zu kopieren sind, 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 implementiert werden 1 . Das unäre Präfix Negation ! sollte (nach den gleichen Regeln) als Mitgliedfunktion implementiert werden. (aber es ist normalerweise keine gute Idee, es zu überladen.)

Die Algorithmen der Standardbibliothek (z. B. std::sort() ) und Typen (z. B. std::map ) erwarten immer nur, dass der operator< vorhanden ist. Die Benutzer Ihres Typs erwarten jedoch, dass auch alle anderen Operatoren vorhanden sind. Wenn Sie also den operator< definieren, müssen Sie die dritte Grundregel der Operatorüberladung befolgen und auch alle anderen booleschen Vergleichsoperatoren definieren. Der kanonische Weg, sie zu implementieren, ist dies:

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

Die wichtige Sache, die hier zu beachten ist, ist, dass nur zwei dieser Operatoren tatsächlich irgendetwas tun, die anderen nur ihre Argumente an eine dieser beiden weiterleiten, um die eigentliche Arbeit zu tun.

Die Syntax für das Ü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 würden .

1 Wie bei allen Faustregeln gibt es manchmal auch Gründe, diese zu brechen. Vergessen Sie in diesem Fall nicht, dass der linke Operand der binären Vergleichsoperatoren, der für Elementfunktionen *this ist, auch const muss. Ein Vergleichsoperator, der als Memberfunktion implementiert ist, müsste also diese Signatur haben:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(Beachten Sie die const am Ende.)

2 Es sollte beachtet werden, dass die integrierte Version von || und && verwenden Abkürzungssemantik. Während die benutzerdefinierten (weil sie syntaktischer Zucker für Methodenaufrufe sind) keine Abkürzungssemantik verwenden. Der Benutzer wird erwarten, dass diese Operatoren eine Abkürzungssemantik haben, und ihr Code kann davon abhängen. Daher wird es NIEMALS empfohlen, sie zu definieren.

Rechenzeichen

Unäre arithmetische Operatoren

Die unären Inkrementierungs- und Dekrementierungsoperatoren kommen sowohl in Präfix- als auch in Postfixaroma vor. Um sich voneinander zu unterscheiden, nehmen die Postfix-Varianten ein zusätzliches dummy int-Argument an. Wenn Sie Inkrement oder Dekrement überladen, stellen Sie sicher, dass Sie sowohl die Präfix- als auch die 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 Minus und Plus ist nicht sehr häufig und wird wahrscheinlich am besten vermieden. Bei Bedarf sollten sie wahrscheinlich als Elementfunktionen überladen werden.

2 Beachten Sie auch, dass die Postfix-Variante mehr Arbeit leistet und daher weniger effizient ist als die Präfix-Variante. Dies ist ein guter Grund, Präfix-Inkrement über Postfix-Inkrement generell zu bevorzugen. Während Compiler normalerweise die zusätzliche Arbeit des Postfixinkrements für eingebaute Typen weg optimieren können, können sie möglicherweise nicht dasselbe für benutzerdefinierte Typen tun (was etwas so unschuldig aussehen könnte wie ein Listeniterator). Sobald Sie sich daran gewöhnt haben, i++ zu tun, wird es sehr schwer, sich daran zu erinnern, ++i zu tun, wenn i nicht von einem integrierten Typ bin (außerdem müssten Sie Code ändern, wenn Sie einen Typ ändern), also ist es besser Machen Sie es sich zur Gewohnheit, Präfix-Inkremente immer zu verwenden, es sei denn, Postfix wird explizit benötigt.

Binäre arithmetische Operatoren

Vergessen Sie bei den binären arithmetischen Operatoren nicht, den dritten Grundregeloperator zu überladen: Wenn Sie + , geben Sie auch += , wenn Sie - , nicht weglassen -= usw. Andrew Koenig soll der Erste gewesen sein zu beobachten, dass die zusammengesetzten Zuweisungsoperatoren als eine Basis für ihre nicht zusammengesetzten Gegenstücke verwendet werden können. Das heißt, Operator + wird in Form von += implementiert, - wird in Form von -= usw. implementiert.

Gemäß unseren Faustregeln sollten + und seine Begleiter keine Mitglieder sein, während ihre Gegenstücke aus der zusammengesetzten Aufgabe ( += usw.), die ihr linkes Argument ändern, ein 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 sein Ergebnis pro Verweis zurück, während operator+ eine Kopie seines Ergebnisses zurückgibt. Natürlich ist das Zurückgeben eines Verweises normalerweise effizienter als das Zurückgeben einer Kopie, aber im Fall von operator+ gibt es keinen Weg um das Kopieren herum. Wenn Sie a + b schreiben, erwarten Sie, dass das Ergebnis ein neuer Wert ist. Deshalb muss operator+ einen neuen Wert zurückgeben. 3 Beachten Sie außerdem, dass operator+ seinen linken Operanden nach Kopie und nicht nach Konstante verwendet. Der Grund dafür ist der gleiche wie der Grund, warum operator= sein Argument pro Kopie nimmt.

Die Bitmanipulationsoperatoren ~ & | ^ << >> sollte auf die gleiche Weise wie die arithmetischen Operatoren implementiert werden. Es gibt jedoch (abgesehen von der Überlastung von << und >> für die Ausgabe und Eingabe) nur sehr wenige sinnvolle Anwendungsfälle, um diese zu überlasten.

3 Auch hieraus ergibt sich, dass a += b im Allgemeinen effizienter ist als a + b und wenn möglich bevorzugt werden sollte.

Array-Subskription

Der Array-Subscript-Operator ist ein binärer Operator, der als Klassenmitglied implementiert werden muss. Es wird für containerartige Typen verwendet, die den Zugriff auf ihre Datenelemente über einen Schlüssel ermöglichen. Die kanonische Form der Bereitstellung dieser ist dies:

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 Datenelemente ändern können, die von 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 eine Kopie anstelle einer const-Referenz zurückgeben.

Operatoren für Zeigerartige Typen

Um eigene Iteratoren oder Smartpointer zu definieren, müssen Sie den unären Präfix-Dereferenzoperator * und den binären Infix-Pointer-Member-Zugriffsoperator -> überladen:

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

Beachten Sie, dass diese auch fast immer eine const und eine nicht-const Version benötigen. Für den Operator -> , wenn value_type vom Typ class (oder struct oder union ) ist, wird ein anderer operator->() rekursiv aufgerufen, bis ein operator->() einen Wert vom Nicht-Klassen-Typ zurückgibt.

Der unäre Adressenoperator sollte niemals überladen werden.

Für operator->*() siehe diese Frage . Es wird selten benutzt und daher selten überladen. Tatsächlich überlasten selbst Iteratoren sie nicht.

Weiter zu Conversion-Operatoren


Überladen von new und delete

Anmerkung: Dies betrifft nur die Syntax des Überladens von new und delete , und nicht die Implementierung solcher überladener Operatoren. Ich denke, dass die Semantik des Überladens von new und delete ihre eigenen FAQ verdient , im Bereich der Überlastung von Betreibern kann ich es niemals gerecht werden.

Grundlagen

Wenn Sie in C ++ einen neuen Ausdruck wie new T(arg) schreiben, passieren zwei Dinge, wenn dieser Ausdruck ausgewertet wird: Der erste operator new wird aufgerufen, um rohen Speicher zu erhalten, und dann wird der entsprechende Konstruktor von T aufgerufen, um diesen rohen Speicher in a umzuwandeln gültiges Objekt Wenn Sie ein Objekt löschen, wird zuerst sein Destruktor aufgerufen und anschließend wird der Speicher an den operator delete .
Mit C ++ können Sie diese beiden Operationen optimieren: Speicherverwaltung und die Konstruktion / Zerstörung des Objekts im zugewiesenen Speicher. Letzteres geschieht durch Schreiben von Konstruktoren und Destruktoren für eine Klasse. Die Feinabstimmung der Speicherverwaltung erfolgt, indem Sie Ihren eigenen operator new und den operator delete schreiben.

Die erste der grundlegenden Regeln des Überladens von Operatoren - tun Sie es nicht - gilt insbesondere für das Überladen new und delete . Fast die einzigen Gründe, diese Operatoren zu überlasten, sind Leistungsprobleme und Speicherbeschränkungen , und in vielen Fällen ergeben andere Aktionen, wie Änderungen an den verwendeten Algorithmen , ein viel höheres Kosten / Gewinn-Verhältnis als der Versuch, die Speicherverwaltung zu optimieren.

Die C ++ - Standardbibliothek enthält eine Reihe vordefinierter Operatoren für new und delete . Die wichtigsten sind diese:

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, und zwar für ein Array von Objekten. Wenn Sie Ihre eigenen Versionen von diesen bereitstellen, werden sie nicht überladen, sondern die aus der Standardbibliothek ersetzen .
Wenn Sie den operator new überladen, sollten Sie auch immer den entsprechenden operator delete , auch wenn Sie ihn nie aufrufen möchten. Der Grund dafür ist, dass, wenn ein Konstruktor während der Auswertung eines neuen Ausdrucks auslöst, das Laufzeitsystem den Speicher an den operator delete , der dem operator new , der zum Zuweisen des Speichers zum Erstellen des Objekts aufgerufen wurde. Wenn Sie dies tun keinen passenden operator delete , der standardmäßig aufgerufen wird, der fast immer falsch ist.
Wenn Sie new und delete überladen, sollten Sie auch die Array-Varianten überlasten.

Platzierung new

C ++ ermöglicht neuen und löschenden Operatoren, zusätzliche Argumente zu übernehmen.
Das so genannte Placement New ermöglicht das Erstellen eines Objekts an einer bestimmten Adresse, die an Folgendes übergeben wird:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

Die Standardbibliothek enthält die entsprechenden Überladungen der neuen Operatoren delete und delete:

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 im obigen Beispielcode für die Platzierung new der operator delete nie aufgerufen wird, es sei denn, der Konstruktor von X löst eine Ausnahme aus.

Sie können auch neu laden und mit anderen Argumenten delete . Wie beim zusätzlichen Argument für die Platzierung neu, werden diese Argumente in Klammern nach dem Schlüsselwort new . Lediglich aus historischen Gründen werden solche Varianten oft auch als Placement New bezeichnet, auch wenn ihre Argumente nicht darin bestehen, ein Objekt an einer bestimmten Adresse zu platzieren.

Klassenspezifisch neu und löschen

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

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

Overloaded thus, new and delete behave like static member functions. For objects of my_class , the std::size_t argument will always be sizeof(my_class) . However, these operators are also called for dynamically allocated objects of derived classes , in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.


Die Entscheidung zwischen Mitglied und Nichtmitglied

Die binären Operatoren = (Zuweisung), [] (Array-Subskription), -> (Memberzugriff), sowie der Operator n-ary () (Funktionsaufruf) müssen immer als Memberfunktionen implementiert werden , da die Syntax der Sprache erfordert sie dazu.

Andere Operatoren können entweder als Mitglieder oder als Nicht-Mitglieder implementiert werden. Einige davon müssen jedoch in der Regel als Nicht-Member-Funktionen implementiert werden, da ihr linker Operand nicht von Ihnen geändert werden kann. Die prominentesten 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 auswählen müssen, ob Sie sie als Elementfunktion oder Nichtmitgliedsfunktion implementieren möchten, können Sie anhand der folgenden Faustregeln entscheiden:

  1. Wenn es ein unärer Operator ist , implementieren Sie es als Member- Funktion.
  2. Wenn ein binärer Operator beide Operanden gleich behandelt (sie bleiben unverändert), implementieren Sie diesen Operator als Nicht-Member- Funktion.
  3. Wenn ein binärer Operator seine beiden Operanden nicht gleich behandelt (normalerweise ändert er seinen linken Operanden), könnte es nützlich sein, ihn zu einer Memberfunktion des Typs seines linken Operanden zu machen, wenn er auf die privaten Teile des Operanden zugreifen muß.

Wie bei allen Faustregeln gibt es natürlich Ausnahmen. Wenn Sie einen Typ haben

enum Month {Jan, Feb, ..., Nov, Dec}

und Sie möchten die Inkrement- und Dekrementoperatoren dafür überladen, Sie können dies nicht als Elementfunktionen ausführen, da Enumerationstypen in C ++ keine Elementfunktionen haben können. Sie müssen es also als freie Funktion überladen. Und der operator<() für eine Klassenschablone, die in einer Klassenvorlage geschachtelt ist, ist viel einfacher zu schreiben und zu lesen, wenn er als Elementfunktion inline in der Klassendefinition ausgeführt wird. Aber das sind in der Tat seltene Ausnahmen.

( Wenn Sie jedoch eine Ausnahme machen, vergessen Sie nicht das Problem von const -ness für den Operanden, das für Member-Funktionen zum impliziten Argument wird. Wenn der Operator als Nicht-Member-Funktion sein äußerstes Argument als Eine const , derselbe Operator wie eine Member-Funktion muss eine const am Ende haben, um *this einer const Referenz zu machen.)

Fahren Sie mit Common Operators fort, um sie zu überlasten .


Die drei Grundregeln des Überladens von Operatoren in C ++

Wenn es zum Überladen von Operatoren in C ++ kommt, gibt es drei grundlegende Regeln, denen Sie folgen sollten . Wie bei allen solchen Regeln gibt es tatsächlich Ausnahmen. Manchmal sind Menschen von ihnen abgewichen und das Ergebnis war kein schlechter Code, aber solche positiven Abweichungen sind selten. Zumindest waren 99 von 100 solcher Abweichungen, die ich gesehen habe, ungerechtfertigt. Es könnte aber genauso gut 999 von 1000 gewesen sein. Sie sollten also besser die folgenden Regeln beachten.

  1. Wenn die Bedeutung eines Operators nicht offensichtlich klar und unbestritten ist, sollte er nicht überladen werden. Stellen Sie stattdessen eine Funktion mit einem gut gewählten Namen bereit.
    Im Grunde sagt die erste und wichtigste Regel zur Überlastung von Betreibern im Kern: Tue es nicht . Das mag seltsam erscheinen, denn es gibt eine Menge über die Überlastung von Operatoren zu wissen, und deshalb beschäftigen sich viele Artikel, Buchkapitel und andere Texte mit all dem. Aber trotz dieser scheinbar offensichtlichen Beweise gibt es nur überraschend wenige Fälle, in denen eine Überlastung des Bedieners angemessen ist . Der Grund ist, dass es eigentlich 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. Entgegen der landläufigen Meinung ist dies kaum der Fall.

  2. Bleiben Sie immer bei der bekannten Semantik des Betreibers.
    C ++ hat keine Einschränkungen für die Semantik von überladenen Operatoren. Ihr Compiler akzeptiert glücklicherweise Code, der den binären + Operator zum Subtrahieren von seinem rechten Operanden implementiert. Die Benutzer eines solchen Operators würden jedoch niemals den Ausdruck a + b vermuten, um a + b zu subtrahieren. Dies setzt natürlich voraus, dass die Semantik des Operators in der Anwendungsdomäne unumstritten ist.

  3. Stellen Sie immer alle aus einer Reihe verwandter Operationen bereit.
    Operatoren sind miteinander und mit anderen Operationen verbunden. Wenn Ihr Typ a + b , erwarten die Benutzer, dass sie auch a += b aufrufen können. Wenn es Präfix increment ++a , wird erwartet, dass auch a++ funktioniert. Wenn sie überprüfen können, ob a < b , werden sie mit Sicherheit erwarten, auch prüfen zu können, ob a > b . Wenn sie Ihren Typ kopieren können, erwarten sie auch, dass die Zuweisung funktioniert.

Weiter zur Entscheidung zwischen Mitglied und Nichtmitglied .


Konvertierungsoperatoren (auch als benutzerdefinierte Conversions bezeichnet)

In C ++ können Sie Konvertierungsoperatoren erstellen, Operatoren, mit denen der Compiler zwischen Ihren Typen und anderen definierten Typen konvertieren kann. 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 (wie die Konvertierung zwischen int und long ) implizit 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, wie Konstruktoren mit einem Argument, sind benutzerdefinierte Konvertierungen. Compiler gewähren eine benutzerdefinierte Konvertierung, wenn versucht wird, einen Aufruf einer überladenen Funktion abzugleichen.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Dies scheint zunächst sehr hilfreich zu sein, aber das Problem dabei ist, dass die implizite Konvertierung sogar einsetzt, wenn es nicht erwartet wird. Im folgenden Code wird void f(const char*) aufgerufen, weil my_string() kein lvalue , also stimmt der erste nicht überein:

void f(my_string&);
void f(const char*);

f(my_string());

Anfänger bekommen das leicht falsch und selbst erfahrene C ++ Programmierer sind manchmal überrascht, weil der Compiler eine Überladung auswählt, die sie nicht vermutet haben. Diese Probleme können durch explizite Konvertierungsoperatoren gemildert werden.

Explizite Konvertierungsoperatoren (C ++ 11)

Anders als implizite Konvertierungsoperatoren treten explizite Konvertierungsoperatoren niemals ein, 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 das explicit . Wenn Sie nun versuchen, den unerwarteten Code von den impliziten Konvertierungsoperatoren auszuführen, erhalten Sie einen Compilerfehler:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Um den expliziten Cast-Operator aufzurufen, müssen Sie static_cast , einen C-Style-Cast oder einen Konstruktorstil-Cast (dh T(value) ) verwenden.

Es gibt jedoch eine Ausnahme: Der Compiler darf implizit in bool konvertieren. Außerdem darf der Compiler keine weitere implizite Konvertierung durchführen, nachdem er in bool konvertiert wurde (ein Compiler darf 2 implizite Konvertierungen gleichzeitig bool , aber maximal 1 benutzerdefinierte Konvertierung).

Da der Compiler keine "Vergangenheit" bool , entfernen explizite Konvertierungsoperatoren jetzt die Notwendigkeit für das Safe Bool-Idiom . Zum Beispiel haben intelligente Zeiger vor C ++ 11 das Safe Bool-Idiom verwendet, um Konvertierungen in ganzzahlige Typen zu verhindern. In C ++ 11 verwenden die Smartpointer stattdessen einen expliziten Operator, da der Compiler nicht implizit in einen Integraltyp konvertiert werden darf, nachdem er einen Typ explizit in bool konvertiert hat.

Weiter zu Überladen new und delete .


Die Erstellung eines C ++ - Programms umfasst drei Schritte:

  1. Preprocessing: Der Präprozessor nimmt eine C ++ Quellcodedatei und behandelt die #include s, #include #define s und andere Präprozessordirektiven. Die Ausgabe dieses Schritts ist eine "reine" C ++ - Datei ohne Vorprozessor-Anweisungen.

  2. Compilation: Der Compiler übernimmt die Ausgabe des Pre-Prozessors und erzeugt daraus eine Objektdatei.

  3. Verknüpfen: Der Linker übernimmt die vom Compiler erzeugten Objektdateien und erzeugt entweder eine Bibliothek oder eine ausführbare Datei.

Vorverarbeitung

Der Präprozessor behandelt die Präprozessordirektiven wie #include und #define . Es ist agnostisch von der Syntax von C ++, weshalb es mit Vorsicht verwendet werden muss.

Es funktioniert jeweils mit einer C ++ - Quelldatei, indem #include Direktiven durch den Inhalt der entsprechenden Dateien ersetzt werden (was normalerweise nur Deklarationen sind), Ersetzen von Makros ( #define ) und Auswählen verschiedener Textabschnitte in Abhängigkeit von #if , #ifdef und #ifndef Direktiven.

Der Präprozessor arbeitet mit einem Strom von Vorverarbeitungstoken. Makrosubstitution ist definiert als Ersetzen von Token durch andere Token (der Operator ## ermöglicht das Zusammenführen von zwei Tokens, wenn es sinnvoll ist).

Nach alldem erzeugt der Präprozessor eine einzelne Ausgabe, die ein Strom von Token ist, der aus den oben beschriebenen Transformationen resultiert. Es fügt auch einige spezielle Markierungen hinzu, die dem Compiler mitteilen, woher die einzelnen Zeilen kommen, damit sie sinnvolle Fehlermeldungen erzeugen können.

Einige Fehler können in diesem Stadium mit geschickter Verwendung der Direktiven #if und #error .

Zusammenstellung

Der Kompilierungsschritt wird an jedem Ausgang des Präprozessors durchgeführt. Der Compiler analysiert den reinen C ++ - Quellcode (jetzt ohne Präprozessor-Direktiven) und konvertiert ihn in Assembler-Code. Ruft dann das zugrunde liegende Back-End (Assembler in der Toolchain) auf, das diesen Code in den Maschinencode einfügt, wodurch eine tatsächliche Binärdatei in einem bestimmten Format erzeugt wird (ELF, COFF, a.out, ...). Diese Objektdatei enthält den kompilierten Code (in binärer Form) der in der Eingabe definierten Symbole. Symbole in Objektdateien werden mit Namen bezeichnet.

Objektdateien können sich auf Symbole beziehen, die nicht definiert sind. Dies ist der Fall, wenn Sie eine Deklaration verwenden und keine Definition dafür angeben. Der Compiler stört das nicht und produziert die Objektdatei, solange der Quellcode wohlgeformt ist.

Mit Compilern können Sie die Kompilierung an dieser Stelle normalerweise beenden. Dies ist sehr nützlich, weil Sie damit jede Quellcodedatei separat kompilieren können. Der Vorteil ist, dass Sie nicht alles neu kompilieren müssen, wenn Sie nur eine einzige Datei ändern.

Die erstellten Objektdateien können in speziellen Archiven, sogenannten statischen Bibliotheken, abgelegt werden, um später die Wiederverwendung zu erleichtern.

In diesem Stadium werden "normale" Compilerfehler, wie Syntaxfehler oder Fehler bei Überladungsfehlern, gemeldet.

Verknüpfung

Der Linker erzeugt die endgültige Kompilierungsausgabe aus den vom Compiler erzeugten Objektdateien. Diese Ausgabe kann entweder eine gemeinsam genutzte (oder dynamische) Bibliothek sein (und während der Name ähnlich ist, haben sie nicht viel mit den bereits erwähnten statischen Bibliotheken gemeinsam) oder eine ausführbare Datei.

Sie verbindet alle Objektdateien, indem sie die Referenzen auf undefinierte Symbole durch die korrekten Adressen ersetzt. Jedes dieser Symbole kann in anderen Objektdateien oder in Bibliotheken definiert werden. Wenn sie in anderen Bibliotheken als der Standardbibliothek definiert sind, müssen Sie dies dem Linker mitteilen.

Zu diesem Zeitpunkt sind die häufigsten Fehler fehlende Definitionen oder doppelte Definitionen. Erstere bedeutet, dass entweder die Definitionen nicht existieren (dh sie sind nicht geschrieben) oder dass die Objektdateien oder Bibliotheken, in denen sie sich befinden, nicht an den Linker übergeben wurden. Letzteres ist offensichtlich: Das gleiche Symbol wurde in zwei verschiedenen Objektdateien oder Bibliotheken definiert.





c++ operators operator-overloading c++-faq