Was sind die Grundregeln und Redewendungen für die Überlastung des Bedieners?

2142

Hinweis: Die Antworten wurden in einer bestimmten Reihenfolge gegeben . Da jedoch viele Benutzer die Antworten nach Stimmen und nicht nach der Zeit sortieren, in der sie gegeben wurden, finden Sie hier einen Index der Antworten in der Reihenfolge, in der sie am sinnvollsten sind:

(Hinweis: Dies ist als Eintrag in die C ++ - FAQ von Stack Overflow gedacht . Wenn Sie die Idee kritisieren möchten, eine FAQ in dieser Form bereitzustellen, ist die Veröffentlichung auf Meta, mit der all dies begonnen hat , der richtige Ort dafür. Antworten auf Diese Frage wird im C ++ - Chatroom überwacht, in dem die FAQ-Idee ursprünglich begann, sodass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die auf die Idee gekommen sind.)

sbi
quelle
63
Wenn wir mit dem C ++ - FAQ-Tag fortfahren möchten, sollten die Einträge auf diese Weise formatiert werden.
John Dibling
Ich habe eine kurze Artikelserie für die deutsche C ++ - Community über das Überladen von Operatoren geschrieben: Teil 1: Das Überladen von Operatoren in C ++ behandelt Semantik, typische Verwendung und Besonderheiten für alle Operatoren. Es gibt einige Überschneidungen mit Ihren Antworten hier, dennoch gibt es einige zusätzliche Informationen. Teil 2 und 3 enthalten ein Tutorial zur Verwendung von Boost.Operators. Möchten Sie, dass ich sie übersetze und als Antworten hinzufüge?
Arne Mertz
Oh, und eine englische Übersetzung ist ebenfalls verfügbar: die Grundlagen und die gängige Praxis
Arne Mertz

Antworten:

1042

Gemeinsame Bediener zu überlasten

Der größte Teil der Arbeit bei Überlastungsbetreibern ist der Kesselplattencode. Das ist kein Wunder, da Operatoren lediglich syntaktischer Zucker sind und ihre eigentliche Arbeit durch einfache Funktionen erledigt werden könnte (und häufig an diese weitergeleitet wird). Es ist jedoch wichtig, dass Sie diesen Kesselplattencode richtig verstehen. Wenn Sie fehlschlagen, wird entweder der Code Ihres Bedieners nicht kompiliert oder der Code Ihrer Benutzer wird nicht kompiliert, oder der Code Ihrer Benutzer verhält sich überraschend.

Aufgabenverwalter

Über die Aufgabe gibt es viel zu sagen. Das meiste davon wurde jedoch bereits in den berühmten Copy-And-Swap-FAQ von GMan erwähnt. Daher überspringe ich das meiste hier und liste nur den perfekten Zuweisungsoperator als Referenz auf:

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

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

Die Bitverschiebungsoperatoren <<und >>, obwohl sie immer noch in der Hardware-Schnittstelle für die von C geerbten Bitmanipulationsfunktionen verwendet werden, sind in den meisten Anwendungen als überlastete Stream-Eingabe- und Ausgabeoperatoren häufiger geworden. Informationen zum Überladen von Anleitungen als Bitmanipulationsoperatoren finden Sie im folgenden Abschnitt über binäre arithmetische Operatoren. Fahren Sie fort, um Ihr eigenes benutzerdefiniertes Format und Ihre Parsing-Logik zu implementieren, wenn Ihr Objekt mit iostreams verwendet wird.

Die Stream-Operatoren gehören zu den am häufigsten überladenen Operatoren binäre Infix-Operatoren, für die die Syntax keine Einschränkung angibt, ob sie Mitglieder oder Nichtmitglieder sein sollen. Da sie ihr linkes Argument ändern (sie ändern den Status des Streams), sollten sie gemäß den Faustregeln als Mitglieder des Typs ihres linken Operanden implementiert werden. Ihre linken Operanden sind jedoch Streams aus der Standardbibliothek, und während die meisten von der Standardbibliothek definierten Stream-Ausgabe- und Eingabeoperatoren tatsächlich als Mitglieder der Stream-Klassen definiert sind, werden Sie beim Implementieren von Ausgabe- und Eingabeoperationen für Ihre eigenen Typen Die Stream-Typen der Standardbibliothek können nicht geändert werden. Aus diesem Grund müssen Sie diese Operatoren für Ihre eigenen Typen als Nichtmitgliedsfunktionen implementieren. Die kanonischen Formen der beiden sind folgende:

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // write obj to stream

  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // read obj from stream

  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);

  return is;
}

Bei der Implementierung operator>>ist das manuelle Festlegen des Stream-Status nur erforderlich, wenn das Lesen selbst erfolgreich war. Das Ergebnis entspricht jedoch nicht den Erwartungen.

Funktionsaufruf-Operator

Der Funktionsaufruf Operator, verwendete Funktionsobjekte zu erstellen, die auch als functors bekannt ist , muss als definiert wird Mitglied Funktion, so dass es immer das implizite hat thisArgument der Member - Funktionen. Davon abgesehen kann es überladen werden, eine beliebige Anzahl zusätzlicher Argumente zu verwenden, einschließlich Null.

Hier ist ein Beispiel für die Syntax:

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

Verwendungszweck:

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

In der gesamten C ++ - Standardbibliothek werden Funktionsobjekte immer kopiert. Ihre eigenen Funktionsobjekte sollten daher billig zu kopieren sein. Wenn ein Funktionsobjekt unbedingt Daten verwenden muss, deren Kopieren teuer ist, ist es besser, diese Daten an anderer Stelle zu speichern und das Funktionsobjekt darauf verweisen zu lassen.

Vergleichsoperatoren

Die binären Infix-Vergleichsoperatoren sollten gemäß den Faustregeln als Nichtmitgliedsfunktionen 1 implementiert werden . Die unäre Präfixnegation !sollte (nach denselben Regeln) als Mitgliedsfunktion implementiert werden. (aber es ist normalerweise keine gute Idee, es zu überladen.)

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

inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return  operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}

Das Wichtigste dabei ist, dass nur zwei dieser Operatoren tatsächlich etwas tun, die anderen leiten ihre Argumente nur an einen dieser beiden weiter, um die eigentliche Arbeit zu erledigen.

Die Syntax zum Überladen der verbleibenden binären booleschen Operatoren ( ||, &&) folgt den Regeln der Vergleichsoperatoren. Es ist jedoch sehr unwahrscheinlich, dass Sie einen vernünftigen Anwendungsfall für diese 2 finden .

1 Wie bei allen Faustregeln kann es manchmal auch Gründe geben, diese zu brechen. Wenn ja, vergessen Sie nicht, dass der linke Operand der binären Vergleichsoperatoren, der für Elementfunktionen sein wird *this, auch sein muss const. Ein als Elementfunktion implementierter Vergleichsoperator müsste also diese Signatur haben:

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

(Beachten Sie das constam Ende.)

2 Es sollte angemerkt werden , dass die integrierte Version von ||und &&Verwendung Verknüpfung Semantik. Während die benutzerdefinierten (weil sie syntaktischer Zucker für Methodenaufrufe sind) keine Verknüpfungssemantik verwenden. Der Benutzer erwartet von diesen Operatoren eine Verknüpfungssemantik, und ihr Code kann davon abhängen. Daher wird dringend empfohlen, sie NIEMALS zu definieren.

Rechenzeichen

Unäre arithmetische Operatoren

Die unären Inkrementierungs- und Dekrementierungsoperatoren sind sowohl in der Präfix- als auch in der Postfix-Variante erhältlich. Um voneinander zu unterscheiden, verwenden die Postfix-Varianten ein zusätzliches Dummy-Int-Argument. Wenn Sie das Inkrementieren oder Dekrementieren überladen, müssen Sie immer sowohl die Präfix- als auch die Postfix-Version implementieren. Hier ist die kanonische Implementierung von Inkrement, Dekrement folgt den gleichen Regeln:

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

Beachten Sie, dass die Postfix-Variante in Bezug auf das Präfix implementiert ist. Beachten Sie auch, dass Postfix eine zusätzliche Kopie erstellt. 2

Das Überladen von unären Minus- und Pluswerten ist nicht sehr häufig und wird wahrscheinlich am besten vermieden. Bei Bedarf sollten sie wahrscheinlich als Mitgliedsfunktionen überladen werden.

2 Beachten Sie auch, dass die Postfix-Variante mehr Arbeit leistet und daher weniger effizient zu verwenden ist als die Präfix-Variante. Dies ist ein guter Grund, das Präfixinkrement im Allgemeinen dem Postfixinkrement vorzuziehen. Während Compiler normalerweise die zusätzliche Arbeit des Postfix-Inkrements für integrierte Typen optimieren können, können sie dies möglicherweise nicht für benutzerdefinierte Typen tun (was so harmlos aussehen könnte wie ein Listeniterator). Sobald Sie sich daran gewöhnt haben i++, wird es sehr schwierig, sich daran zu erinnern, ++istattdessen etwas zu tun, wenn ies sich nicht um einen integrierten Typ handelt (und Sie müssten den Code ändern, wenn Sie einen Typ ändern). Daher ist es besser, sich immer daran zu gewöhnen Verwenden des Präfixinkrements, sofern nicht ausdrücklich ein Postfix benötigt wird.

Binäre arithmetische Operatoren

Vergessen Sie bei den binären arithmetischen Operatoren nicht, die Überladung des dritten Grundregeloperators zu beachten : Wenn Sie angeben +, geben Sie auch an +=, wenn Sie angeben -, lassen Sie nicht weg -=usw. Andrew Koenig soll als erster festgestellt haben, dass die zusammengesetzte Zuordnung Operatoren können als Basis für ihre nicht zusammengesetzten Gegenstücke verwendet werden. Das heißt, der Operator +wird in Bezug auf implementiert +=, -wird in Bezug auf implementiert -=usw.

Gemäß unseren Faustregeln sollten +seine Begleiter Nichtmitglieder sein, während ihre Gegenstücke für die zusammengesetzte Zuweisung ( +=usw.), die ihr linkes Argument ändern, Mitglied sein sollten. Hier ist der beispielhafte Code für +=und +; Die anderen binären arithmetischen Operatoren sollten auf die gleiche Weise implementiert werden:

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}

operator+=Gibt das Ergebnis pro Referenz zurück, während operator+eine Kopie des Ergebnisses zurückgegeben wird. Natürlich ist das Zurücksenden einer Referenz normalerweise effizienter als das Zurücksenden einer Kopie, aber im Fall von operator+führt kein Weg an dem Kopieren vorbei. Wenn Sie schreiben a + b, erwarten Sie, dass das Ergebnis ein neuer Wert ist, weshalb operator+ein neuer Wert zurückgegeben werden muss. 3 Beachten Sie auch, dass operator+der linke Operand durch Kopieren und nicht durch Konstantenreferenz verwendet wird. Der Grund dafür ist der gleiche wie der Grund für operator=die Argumentation pro Kopie.

Die Bitmanipulationsoperatoren ~ & | ^ << >>sollten auf die gleiche Weise wie die arithmetischen Operatoren implementiert werden. Es gibt jedoch (mit Ausnahme der Überladung <<sowie der >>Ausgabe und Eingabe) nur sehr wenige vernünftige Anwendungsfälle für die Überladung dieser.

3 Die Lehre daraus ist wiederum, dass sie a += bim Allgemeinen effizienter ist als a + bund wenn möglich bevorzugt werden sollte.

Array-Subskription

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

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

Sofern Sie nicht möchten, dass Benutzer Ihrer Klasse die von zurückgegebenen Datenelemente ändern können operator[](in diesem Fall können Sie die nicht konstante Variante weglassen), sollten Sie immer beide Varianten des Operators angeben.

Wenn bekannt ist, dass value_type auf einen integrierten Typ verweist, sollte die const-Variante des Operators besser eine Kopie anstelle einer const-Referenz zurückgeben:

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

Operatoren für zeigerähnliche Typen

Um Ihre eigenen Iteratoren oder intelligenten Zeiger zu definieren, müssen Sie den unären Präfix-Dereferenzierungsoperator *und den Zugriffsoperator für binäre Infixzeiger überladen ->:

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

Beachten Sie, dass auch diese fast immer sowohl eine const- als auch eine non-const-Version benötigen. Wenn der ->Operator value_typevom Typ class(oder structoder union) ist, wird ein anderer operator->()rekursiv aufgerufen, bis a operator->()einen Wert vom Typ Nichtklasse zurückgibt.

Die unäre Adresse des Operators sollte niemals überladen werden.

Für operator->*()sieht diese Frage . Es wird selten benutzt und ist daher selten überlastet. Tatsächlich überladen es selbst Iteratoren nicht.


Fahren Sie mit den Konvertierungsoperatoren fort

sbi
quelle
89
operator->()ist eigentlich extrem komisch. Es ist nicht erforderlich, a zurückzugeben. value_type*Tatsächlich kann ein anderer Klassentyp zurückgegeben werden, vorausgesetzt, der Klassentyp hat einenoperator->() , der anschließend aufgerufen wird. Dieser rekursive Aufruf von operator->()s wird fortgesetzt, bis ein value_type*Rückgabetyp auftritt. Wahnsinn! :)
j_random_hacker
2
Es geht nicht gerade um Effektivität. Es geht darum, dass wir es in (sehr) wenigen Fällen nicht auf traditionell-idiomatische Weise tun können: Wenn die Definition beider Operanden unverändert bleiben muss, während wir das Ergebnis berechnen. Und wie gesagt, es gibt zwei klassische Beispiele: Matrizenmultiplikation und Multiplikation von Polynomen. Wir könnten *in Bezug auf definieren, *=aber es wäre umständlich, weil eine der ersten Operationen von *=wäre, ein neues Objekt zu erstellen, Ergebnis der Berechnung. Dann, nach der for-ijk-Schleife, würden wir dieses temporäre Objekt mit tauschen *this. dh. 1.copy, 2.operator *, 3.swap
Luc Hermitte
6
Ich bin nicht einverstanden mit den const / non-const-Versionen Ihrer zeigerähnlichen Operatoren, z. B. `const value_type & operator * () const;` - dies wäre wie eine T* constRückgabe einer const T&On-Dereferenzierung, was nicht der Fall ist. Oder mit anderen Worten: Ein const-Zeiger impliziert keinen const-Zeiger. In der Tat ist es nicht trivial zu imitieren T const *- was der Grund für das ganze const_iteratorZeug in der Standardbibliothek ist. Fazit: Die Unterschrift sollte seinreference_type operator*() const; pointer_type operator->() const
Arne Mertz
6
Ein Kommentar: Die vorgeschlagene Implementierung von binären arithmetischen Operatoren ist nicht so effizient wie möglich. Se Boost-Operatoren-Header-Simmetrie-Hinweis: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Eine weitere Kopie kann vermieden werden, wenn Sie eine lokale Kopie des ersten Parameters verwenden, do + =, und das zurückgeben lokale Kopie. Dies ermöglicht die NRVO-Optimierung.
Manu343726
3
Wie ich im Chat erwähnt habe, L <= Rkann auch als !(R < L)statt ausgedrückt werden !(L > R). Könnte eine zusätzliche Inlining-Ebene in schwer zu optimierenden Ausdrücken einsparen (und so implementiert es auch Boost.Operators).
TemplateRex
494

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

Wenn es um das Überladen von Operatoren in C ++ geht, sollten Sie drei Grundregeln befolgen . 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 99 von 100 solchen Abweichungen, die ich gesehen habe, waren 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 offensichtlich klar 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 für das Überladen von Bedienern im Kern: Tun Sie es nicht . Das mag seltsam erscheinen, da über das Überladen von Operatoren viel zu wissen ist und sich daher viele Artikel, Buchkapitel und andere Texte mit all dem befassen. Trotz dieser scheinbar offensichtlichen Beweise gibt es nur überraschend wenige Fälle, in denen eine Überlastung des Bedieners 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. Entgegen der landläufigen Meinung ist dies kaum jemals der Fall.

  2. Halten Sie sich immer an die bekannte Semantik des Bedieners.
    C ++ unterliegt keinen Einschränkungen hinsichtlich der Semantik überladener Operatoren. Ihr Compiler akzeptiert gerne Code, der den Binäroperator implementiert+, um von seinem rechten Operanden zu subtrahieren. Jedoch würden die Nutzer eines solchen Operators nie den Ausdruck vermutena + bsubtrahierenaausb. Dies setzt natürlich voraus, dass die Semantik des Operators in der Anwendungsdomäne unbestritten ist.

  3. Stellen Sie immer alles aus einer Reihe verwandter Vorgänge bereit.
    Operatoren sind miteinander und mit anderen Operationen verbunden. Wenn Ihr Typ dies unterstützta + b, erwarten Benutzer, dass sie auch anrufena += bkönnen. Wenn es das Präfixinkrement unterstützt++a, wird erwarteta++,dasses auch funktioniert. Wenn sie prüfen können, oba < b, können sie mit Sicherheit auch erwarten,obsie prüfen können, oba > b. Wenn sie Ihren Typ kopieren und konstruieren können, erwarten sie, dass die Zuweisung ebenfalls funktioniert.


Fahren Sie mit der Entscheidung zwischen Mitglied und Nichtmitglied fort .

sbi
quelle
16
Das einzige, von dem ich weiß, dass es gegen eines dieser boost::spiritElemente verstößt, ist lol.
Billy ONeal
66
@ Billy: Laut einigen ist der Missbrauch der +Verkettung von Strings eine Verletzung, aber mittlerweile hat sich die Praxis etabliert, so dass es natürlich erscheint. Obwohl ich mich an eine selbstgebraute Saitenklasse erinnere, die ich in den 90er Jahren gesehen habe und die &zu diesem Zweck Binärdateien verwendete ( siehe BASIC für die etablierte Praxis). Aber ja, wenn man es in die Standardbibliothek setzt, ist das im Grunde genommen in Stein gemeißelt. Das gleiche gilt für Missbrauch <<und >>für IO, übrigens. Warum sollte Linksverschiebung die offensichtliche Ausgabeoperation sein? Weil wir alle davon erfahren haben, als wir unser erstes "Hallo Welt!" Anwendung. Und aus keinem anderen Grund.
sbi
5
@curiousguy: Wenn du es erklären musst, ist es nicht offensichtlich klar und unbestritten. Ebenso, wenn Sie die Überlastung diskutieren oder verteidigen müssen.
sbi
5
@sbi: "Peer Review" ist immer eine gute Idee. Für mich unterscheidet sich ein schlecht gewählter Operator nicht von einem schlecht gewählten Funktionsnamen (ich habe viele gesehen). Operator sind nur Funktionen. Nicht mehr und nicht weniger. Regeln sind genauso. Und um zu verstehen, ob eine Idee gut ist, ist es am besten zu verstehen, wie lange es dauert, bis sie verstanden wird. (Daher ist Peer Review ein Muss, aber Peers müssen zwischen Menschen ausgewählt werden, die frei von Dogmen und Vorurteilen sind.)
Emilio Garavaglia
5
@sbi Für mich ist die einzige absolut offensichtliche und unbestreitbare Tatsache, operator==dass es sich um eine Äquivalenzbeziehung handeln sollte (IOW, Sie sollten kein nicht signalisierendes NaN verwenden). Es gibt viele nützliche Äquivalenzbeziehungen für Container. Was bedeutet Gleichheit? " agleich b" bedeutet das aund bhat den gleichen mathematischen Wert. Das Konzept des mathematischen Werts eines (Nicht-NaN) floatist klar, aber der mathematische Wert eines Containers kann viele verschiedene (typrekursive) nützliche Definitionen haben. Die stärkste Definition von Gleichheit ist "sie sind dieselben Objekte" und sie ist nutzlos.
Neugieriger
265

Die allgemeine Syntax der Operatorüberladung 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 1 überladen werden . Das heißt, mindestens einer der Operanden muss vom 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 ++,?:

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)
  • Bitmanipulation: & | ^ << >>und &= |= ^= <<= >>=(alle binären Infixe); ~(unäres Präfix)
  • Boolesche Algebra: == != < > <= >= || &&(alle binären Infixe); !(unäres Präfix)
  • Speicherverwaltung: new new[] delete delete[]
  • implizite Konvertierungsoperatoren
  • Verschiedenes: = [] -> ->* , (alle binären Infixe); * &(alle unären Präfixe) ()(Funktionsaufruf, n-ary Infix)

Die Tatsache, dass Sie all dies überlasten können, bedeutet jedoch nicht, dass Sie dies tun sollten . Siehe 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 Nichtmitgliedsfunktionen implementiert werden . Ob Sie frei wählen können oder gebunden sind, hängt von mehreren Kriterien ab. 2 Ein unärer Operator @3 , der auf ein Objekt x angewendet wird, wird entweder als operator@(x)oder als aufgerufen x.operator@(). Ein binärer Infix-Operator @, der auf die Objekte xund angewendet wird y, wird entweder als operator@(x,y)oder als aufgerufen x.operator@(y). 4

Operatoren, die als Nichtmitgliedsfunktionen implementiert sind, sind manchmal mit dem Typ ihres Operanden befreundet.

1 Der 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 derjenigen aus der Standardbibliothek, obwohl sie als solche nicht von Benutzern definiert werden.

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

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

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


Fahren Sie mit den drei Grundregeln für das Überladen von Operatoren in C ++ fort .

sbi
quelle
~ist ein unäres Präfix, kein binäres Infix.
Mrkj
1
.*fehlt in der Liste der nicht überladbaren Operatoren.
Celticminstrel
1
@Mateen Ich wollte einen Platzhalter anstelle eines echten Operators verwenden, um zu verdeutlichen, dass es sich nicht um einen speziellen Operator handelt, sondern für alle. Und wenn Sie ein C ++ - Programmierer sein möchten, sollten Sie lernen, auch auf das Kleingedruckte zu achten. :)
sbi
1
@HR: Wenn Sie diesen Leitfaden gelesen hätten, würden Sie wissen, was los ist. Ich empfehle generell, dass Sie die ersten drei Antworten lesen, die mit der Frage verknüpft sind. Das sollte nicht mehr als eine halbe Stunde Ihres Lebens sein und gibt Ihnen ein grundlegendes Verständnis. Die betreiberspezifische Syntax, die Sie später nachschlagen können. Ihr spezifisches Problem deutet darauf hin, dass Sie versuchen, operator+()als Mitgliedsfunktion zu überladen , ihm jedoch die Signatur einer freien Funktion gegeben haben. Siehe hier .
sbi
1
@sbi: Ich habe die drei ersten Beiträge bereits gelesen und danke, dass du sie gemacht hast. :) Ich werde versuchen, das Problem zu lösen, sonst denke ich, dass es besser ist, es auf einer separaten Frage zu stellen. Nochmals vielen Dank, dass Sie uns das Leben so einfach gemacht haben! : D
Hosein Rahnama
251

Die Entscheidung zwischen Mitglied und Nichtmitglied

Die binären Operatoren =(Zuweisung), [](Array-Abonnement), ->(Elementzugriff) sowie der n-ary- ()Operator (Funktionsaufruf) müssen immer als Elementfunktionen implementiert werden , da dies aufgrund der Syntax der Sprache erforderlich ist.

Andere Operatoren können entweder als Mitglieder oder als Nichtmitglieder implementiert werden. Einige von ihnen müssen jedoch normalerweise als Nichtmitgliedsfunktionen 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.

Verwenden Sie für alle Operatoren, bei denen Sie auswählen müssen, ob sie entweder als Elementfunktion oder als Nichtmitgliedsfunktion implementiert werden sollen, die folgenden Faustregeln , um zu entscheiden:

  1. Wenn es sich um ein unärer Operator , implementieren sie als Mitglied 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 nicht beide Operanden gleich behandelt (normalerweise ändert er seinen linken Operanden), kann es nützlich sein, ihn zu einer Mitgliedsfunktion des Typs seines linken Operanden zu machen, wenn er auf die privaten Teile des Operanden zugreifen muss.

Natürlich gibt es wie bei allen Faustregeln Ausnahmen. Wenn Sie einen Typ haben

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

Wenn Sie die Inkrementierungs- und Dekrementierungsoperatoren dafür überladen möchten, können Sie dies nicht als Elementfunktionen ausführen, da in C ++ Aufzählungstypen keine Elementfunktionen haben können. Sie müssen es also als freie Funktion überladen. Und operator<()für eine in einer Klassenvorlage verschachtelte Klassenvorlage ist das Schreiben und Lesen viel einfacher, wenn sie als Elementfunktion inline in der Klassendefinition ausgeführt wird. Dies sind jedoch in der Tat seltene Ausnahmen.

( Wenn Sie jedoch eine Ausnahme machen, vergessen Sie nicht das Problem const-ness für den Operanden, der für Elementfunktionen zum impliziten thisArgument wird. Wenn der Operator als Nichtmitgliedsfunktion das Argument ganz links als constReferenz verwenden würde muss derselbe Operator wie eine Mitgliedsfunktion constam Ende ein haben, um *thiseine constReferenz zu erstellen.)


Fahren Sie mit Common Operators fort, um zu überladen .

sbi
quelle
9
Herb Sutters Artikel in Effective C ++ (oder sind es C ++ - Codierungsstandards?) Sagt, dass man Nicht-Mitglieder-Nicht-Freund-Funktionen gegenüber Mitgliedsfunktionen bevorzugen sollte, um die Kapselung der Klasse zu erhöhen. Meiner Meinung nach hat der Grund für die Kapselung Vorrang vor Ihrer Faustregel, verringert jedoch nicht den Qualitätswert Ihrer Faustregel.
Paercebal
8
@paercebal: Effektives C ++ ist von Meyers, C ++ Coding Standards von Sutter. Auf welches beziehen Sie sich? Jedenfalls mag ich die Idee, beispielsweise operator+=()kein Mitglied zu sein , nicht. Es muss seinen linken Operanden ändern, also muss es per Definition tief in seine Innereien graben. Was würden Sie gewinnen, wenn Sie kein Mitglied wären?
sbi
9
@sbi: Punkt 44 in C ++ Coding Standards (Sutter) Schreiben Sie lieber Nicht-Mitglieder-Funktionen als Freunde , natürlich gilt dies nur, wenn Sie diese Funktion tatsächlich nur über die öffentliche Schnittstelle der Klasse schreiben können. Wenn Sie nicht können (oder können, aber es würde die Leistung stark beeinträchtigen), müssen Sie es entweder zum Mitglied oder zum Freund machen.
Matthieu M.
3
@sbi: Ups, effektiv, außergewöhnlich ... Kein Wunder, dass ich die Namen verwechsle. Der Gewinn besteht jedoch darin, die Anzahl der Funktionen, die Zugriff auf private / geschützte Objektdaten haben, so weit wie möglich zu begrenzen. Auf diese Weise erhöhen Sie die Kapselung Ihrer Klasse und erleichtern deren Wartung / Test / Entwicklung.
Paercebal
12
@sbi: Ein Beispiel. Angenommen, Sie codieren eine String-Klasse mit operator +=den appendMethoden und. Die appendMethode ist vollständiger, da Sie eine Teilzeichenfolge des Parameters von Index i an Index n -1 anhängen können : append(string, start, end)Es erscheint logisch, einen +=Aufruf mit start = 0und anhängen zu lassen end = string.size. In diesem Moment könnte append eine Member-Methode sein, muss aber operator +=kein Member sein, und wenn Sie es zu einem Nicht-Member machen, wird die Menge an Code, die mit den String-Innereien spielt, verringert. Es ist also eine gute Sache. ^ _ ^ ...
paercebal
165

Conversion-Operatoren (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 implizit (wie die Konvertierung zwischen intund 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*() )

Dies scheint zunächst sehr hilfreich zu sein, aber das Problem dabei ist, dass die implizite Konvertierung sogar dann einsetzt, wenn dies nicht erwartet wird. Im folgenden Code void f(const char*)wird aufgerufen, da my_string()es sich nicht um einen l-Wert handelt , sodass der erste nicht übereinstimmt:

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: Hinweis: Kandidaten sind:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: Hinweis: Keine bekannte Konvertierung für Argument 1 von 'my_string' in 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: Hinweis: Keine bekannte Konvertierung für Argument 1 von 'my_string' nach 'const char *'

Um den expliziten Cast-Operator aufzurufen, müssen Sie einen static_castCast im C-Stil oder einen Cast im Castor-Stil (dh T(value)) verwenden.

Es gibt jedoch eine Ausnahme: Der Compiler darf implizit in konvertieren bool. Darüber hinaus darf der Compiler nach der Konvertierung in keine weitere implizite Konvertierung durchführen bool(ein Compiler darf jeweils 2 implizite Konvertierungen durchführen, maximal jedoch nur eine benutzerdefinierte Konvertierung).

Da der Compiler "past" nicht umwandelt bool, wird durch explizite Konvertierungsoperatoren jetzt die Safe Bool-Sprache nicht mehr benötigt . Beispielsweise verwendeten intelligente Zeiger vor C ++ 11 das Safe Bool-Idiom, um Konvertierungen in integrale Typen zu verhindern. In C ++ 11 verwenden die intelligenten Zeiger stattdessen einen expliziten Operator, da der Compiler nicht implizit in einen integralen Typ konvertieren darf, nachdem er einen Typ explizit in bool konvertiert hat.

Weiter zum Überladen newunddelete .

JKor
quelle
148

Überladung newunddelete

Hinweis: Dies betrifft nur die Syntax der Überladungnewunddeletenicht die Implementierung solcher überladenen Operatoren. Ich denke, dass die Semantik der Überladungnew und deleteverdient ihre eigenen FAQ , innerhalb des Themas der Überladung von Operatoren kann ich es nie gerecht werden.

Grundlagen

In C ++, wenn Sie schreiben ein neuer Ausdruck wie new T(arg)zwei Dinge passieren , wenn dieser Ausdruck ausgewertet wird: Zuerst operator newaufgerufen wird roh Speicher zu erhalten, und dann die entsprechende Konstruktor Taufgerufen wird diesen rohen Speicher in ein gültiges Objekt zu drehen. Wenn Sie ein Objekt löschen, wird zunächst dessen Destruktor aufgerufen und anschließend der Speicher zurückgegeben operator delete.
Mit C ++ können Sie beide Vorgänge optimieren: Speicherverwaltung und Aufbau / Zerstörung des Objekts im zugewiesenen Speicher. Letzteres erfolgt durch Schreiben von Konstruktoren und Destruktoren für eine Klasse. Die Feinabstimmung der Speicherverwaltung erfolgt durch Schreiben Ihrer eigenen operator newund operator delete.

Die erste der Grundregeln für das Überladen von Bedienern - tun Sie es nicht - gilt insbesondere für das Überladen von newund delete. Fast die einzigen Gründe für die Überlastung dieser Operatoren sind Leistungsprobleme und Speicherbeschränkungen. In vielen Fällen bieten andere Aktionen, wie z. B. Ä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 newund deleteOperatoren. 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 weisen Speicher für ein Objekt zu / freigeben, die beiden letzteren für ein Array von Objekten. Wenn Sie Ihre eigenen Versionen davon bereitstellen, werden diese nicht überladen, sondern ersetzen diejenigen aus der Standardbibliothek.
Wenn Sie überladen operator new, sollten Sie das Matching immer auch überladen operator delete, auch wenn Sie nie vorhaben, es aufzurufen. Der Grund ist, dass, wenn ein Konstruktor während der Auswertung eines neuen Ausdrucks auslöst, das Laufzeitsystem den Speicher auf die operator deleteÜbereinstimmung zurücksetzt, die operator newaufgerufen wurde, um den Speicher zuzuweisen, in dem das Objekt erstellt werden soll. Wenn Sie keine Übereinstimmung angeben operator deletewird die Standardeinstellung aufgerufen, was fast immer falsch ist.
Wenn Sie newund überladen delete, sollten Sie auch die Array-Varianten überladen.

Platzierung new

Mit C ++ können neue und gelöschte Operatoren zusätzliche Argumente annehmen.
Mit der sogenannten Platzierung neu können Sie ein Objekt an einer bestimmten Adresse erstellen, 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 dazu die entsprechenden Überladungen der Operatoren new 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 oben angegebenen Beispielcode für die Platzierung new operator deleteniemals aufgerufen wird, es sei denn, der Konstruktor von X löst eine Ausnahme aus.

Sie können auch überladen newund deletemit anderen Argumenten. Wie beim zusätzlichen Argument für die Platzierung neu werden diese Argumente auch in Klammern nach dem Schlüsselwort aufgeführt new. Lediglich aus historischen Gründen werden solche Varianten häufig auch als neue Platzierung bezeichnet, auch wenn ihre Argumente nicht dazu dienen, ein Objekt an einer bestimmten Adresse zu platzieren.

Klassenspezifisch neu und löschen

Am häufigsten sollten Sie die Speicherverwaltung optimieren, da die Messung gezeigt hat, dass Instanzen einer bestimmten Klasse oder einer Gruppe verwandter Klassen häufig erstellt und zerstört werden und die Standardspeicherverwaltung des Laufzeitsystems optimiert ist allgemeine Leistung, handelt ineffizient in diesem speziellen Fall. Um dies zu verbessern, können Sie new für eine bestimmte Klasse überladen und 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);
    // ... 
}; 

So überladen verhalten sich Neu und Löschen wie statische Elementfunktionen. Für Objekte von my_classwird das std::size_tArgument immer sein sizeof(my_class). Diese Operatoren werden jedoch auch für dynamisch zugewiesene Objekte abgeleiteter Klassen aufgerufen. In diesem Fall kann sie größer sein.

Global neu und löschen

Um das globale Neu zu löschen und zu löschen, ersetzen Sie einfach die vordefinierten Operatoren der Standardbibliothek durch unsere eigenen. Dies muss jedoch selten getan werden.

sbi
quelle
11
Ich bin auch nicht der Meinung, dass das Ersetzen des globalen Operators new und delete normalerweise der Leistung dient. Im Gegenteil, es dient normalerweise der Fehlerverfolgung.
Yttrill
1
Beachten Sie außerdem, dass Sie bei Verwendung eines überladenen neuen Operators auch einen Löschoperator mit übereinstimmenden Argumenten angeben müssen. Sie sagen, dass in dem Abschnitt über globales Neu / Löschen, wo es nicht von großem Interesse ist.
Yttrill
13
@Yttrill Sie verwirren Dinge. Die Bedeutung wird überladen. Was "Operatorüberladung" bedeutet, ist, dass die Bedeutung überladen ist. Dies bedeutet nicht, dass Funktionen im wahrsten Sinne des Wortes überlastet sind, und insbesondere Operator New wird die Version des Standards nicht überladen. @sbi behauptet nicht das Gegenteil. Es ist üblich, es als "Überladen neu" zu bezeichnen, genauso wie es üblich ist, "Überladungsadditionsoperator" zu sagen.
Johannes Schaub - Litb
1
@sbi: Siehe (oder besser Link zu) gotw.ca/publications/mill15.htm . Es ist nur eine gute Praxis gegenüber Menschen, die manchmal nothrowneue verwenden.
Alexandre C.
1
"Wenn Sie keinen passenden Operator löschen angeben, heißt der Standard" -> Wenn Sie Argumente hinzufügen und keinen passenden Löschvorgang erstellen, wird überhaupt kein Operatorlöschen aufgerufen, und Sie haben einen Speicherverlust. (15.2.2, der vom Objekt belegte Speicher wird nur freigegeben, wenn ein geeigneter ... Operator gelöscht wurde)
dascandy
46

Warum kann die operator<<Funktion zum Streamen von Objekten in std::coutoder in eine Datei keine Mitgliedsfunktion sein?

Angenommen, Sie haben:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Aus diesem Grund können Sie nicht verwenden:

Foo f = {10, 20.0};
std::cout << f;

Da operator<<als Mitgliedsfunktion von überladen ist Foo, muss die LHS des Operators ein FooObjekt 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.

R Sahu
quelle