Kopierer und = Operatorüberladung in C ++ kopieren: Ist eine gemeinsame Funktion möglich?

87

Da ein Kopierkonstruktor

MyClass(const MyClass&);

und eine = Bedienerüberlastung

MyClass& operator = (const MyClass&);

Haben Sie so ziemlich den gleichen Code, den gleichen Parameter und unterscheiden Sie sich nur bei der Rückgabe. Ist es möglich, eine gemeinsame Funktion für beide zu haben?

MPelletier
quelle
6
"... haben so ziemlich den gleichen Code ..."? Hmm ... Sie müssen etwas falsch machen. Versuchen Sie, die Notwendigkeit zu minimieren, dafür benutzerdefinierte Funktionen zu verwenden, und lassen Sie den Compiler die ganze Drecksarbeit erledigen. Dies bedeutet häufig, dass Ressourcen in ihrem eigenen Mitgliedsobjekt gekapselt werden. Sie könnten uns einen Code zeigen. Vielleicht haben wir einige gute Designvorschläge.
Sellibitze

Antworten:

121

Ja. Es gibt zwei gängige Optionen. Eine - von der im Allgemeinen abgeraten wird - besteht darin, den operator=vom Kopierkonstruktor explizit aufzurufen :

MyClass(const MyClass& other)
{
    operator=(other);
}

Die Bereitstellung eines Gutes operator=ist jedoch eine Herausforderung, wenn es darum geht, mit dem alten Zustand und Problemen umzugehen, die sich aus der Selbstzuweisung ergeben. Außerdem werden alle Mitglieder und Basen zuerst standardmäßig initialisiert, auch wenn sie von zugewiesen werden sollen other. Dies gilt möglicherweise nicht für alle Mitglieder und Basen, und selbst wenn es gültig ist, ist es semantisch redundant und kann praktisch teuer sein.

Eine zunehmend beliebte Lösung ist die Implementierung operator=mit dem Kopierkonstruktor und einer Swap-Methode.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

oder auch:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Eine swapFunktion ist normalerweise einfach zu schreiben, da sie nur den Besitz der Interna austauscht und nicht den vorhandenen Status bereinigen oder neue Ressourcen zuweisen muss.

Der Vorteil der Kopier- und Swap-Sprache besteht darin, dass sie automatisch selbstzuweisungssicher ist und - vorausgesetzt, der Swap-Vorgang ist kein Wurf - auch stark ausnahmesicher ist.

Um eine starke Ausnahmesicherheit zu gewährleisten, muss ein handgeschriebener Zuweisungsoperator normalerweise eine Kopie der neuen Ressourcen zuweisen, bevor die Zuweisung der alten Ressourcen des Empfängers aufgehoben wird, damit der alte Status weiterhin wiederhergestellt werden kann, wenn eine Ausnahme beim Zuweisen der neuen Ressourcen auftritt . All dies ist mit Copy-and-Swap kostenlos, ist jedoch in der Regel komplexer und daher fehleranfällig und kann von Grund auf neu ausgeführt werden.

Das eine, worauf Sie achten müssen, ist sicherzustellen, dass die Swap-Methode ein echter Swap ist und nicht die Standardeinstellung, bei std::swapder der Kopierkonstruktor und der Zuweisungsoperator selbst verwendet werden.

In der Regel wird ein Mitglied swapverwendet. std::swapfunktioniert und ist bei allen Grundtypen und Zeigertypen garantiert. Die meisten intelligenten Zeiger können auch mit einer No-Throw-Garantie ausgetauscht werden.

CB Bailey
quelle
3
Eigentlich sind sie keine gewöhnlichen Operationen. Während der Kopierer die Elemente des Objekts zum ersten Mal initialisiert, überschreibt der Zuweisungsoperator vorhandene Werte. In Anbetracht dessen ist das Alling operator=aus dem Kopiercode in der Tat ziemlich schlecht, da zunächst alle Werte auf einen Standardwert initialisiert werden, um sie unmittelbar danach mit den Werten des anderen Objekts zu überschreiben.
sbi
14
Vielleicht zu "Ich empfehle nicht", hinzufügen "und auch kein C ++ - Experte". Jemand könnte mitkommen und nicht erkennen, dass Sie nicht nur eine persönliche Minderheitspräferenz ausdrücken, sondern die festgelegte Konsensmeinung derer, die tatsächlich darüber nachgedacht haben. Und, OK, vielleicht irre ich mich und ein C ++ - Experte empfiehlt es, aber ich persönlich würde immer noch den Handschuh legen, damit jemand eine Referenz für diese Empfehlung findet.
Steve Jessop
4
Fair genug, ich habe dich sowieso schon positiv bewertet :-). Ich denke, wenn etwas allgemein als Best Practice angesehen wird, ist es am besten, dies zu sagen (und es noch einmal anzusehen, wenn jemand sagt, dass es doch nicht wirklich das Beste ist). Wenn jemand gefragt wird, ob es möglich ist, Mutexe in C ++ zu verwenden, würde ich nicht sagen, dass eine häufig verwendete Option darin besteht, RAII vollständig zu ignorieren und nicht ausnahmesicheren Code zu schreiben, der in der Produktion blockiert, aber das Schreiben wird immer beliebter anständiger, funktionierender Code ";-)
Steve Jessop
4
+1. Und ich denke, es gibt immer eine Analyse in Not. Ich denke, es ist vernünftig, eine assignMitgliedsfunktion zu haben, die in einigen Fällen sowohl vom Kopierer als auch vom Zuweisungsoperator verwendet wird (für Lightweight-Klassen). In anderen Fällen (ressourcenintensiv / Fälle verwenden, Handle / Body) ist ein Kopieren / Tauschen natürlich der richtige Weg.
Johannes Schaub - Litb
2
@litb: Ich war davon überrascht, also habe ich Punkt 41 in Ausnahme C ++ nachgeschlagen (in den dies verwandelt wurde) und diese spezielle Empfehlung ist weg und er empfiehlt das Kopieren und Tauschen an seiner Stelle. Ziemlich hinterhältig hat er gleichzeitig "Problem Nr. 4: Es ist ineffizient für die Zuweisung" fallen lassen.
CB Bailey
13

Der Kopierkonstruktor führt die erstmalige Initialisierung von Objekten durch, die früher Rohspeicher waren. Der Zuweisungsoperator OTOH überschreibt vorhandene Werte durch neue. Meistens werden dabei alte Ressourcen (z. B. Speicher) verworfen und neue zugewiesen.

Wenn es eine Ähnlichkeit zwischen den beiden gibt, führt der Zuweisungsoperator die Zerstörung und die Kopierkonstruktion durch. Einige Entwickler implementierten die Zuweisung tatsächlich durch direkte Zerstörung, gefolgt von der Erstellung von Platzierungskopien. Dies ist jedoch eine sehr schlechte Idee. (Was ist, wenn dies der Zuweisungsoperator einer Basisklasse ist, der während der Zuweisung einer abgeleiteten Klasse aufgerufen wurde?)

Was heutzutage normalerweise als kanonische Redewendung angesehen wird, ist die Verwendung, swapwie Charles vorgeschlagen hat:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Dies verwendet Kopierkonstruktion (Anmerkung, otherdie kopiert wird) und Zerstörung (sie wird am Ende der Funktion zerstört) - und sie verwendet sie auch in der richtigen Reihenfolge: Konstruktion (könnte fehlschlagen) vor Zerstörung (darf nicht fehlschlagen).

sbi
quelle
Sollte swapdeklariert werden virtual?
1
@Johannes: Virtuelle Funktionen werden in polymorphen Klassenhierarchien verwendet. Zuweisungsoperatoren werden für Werttypen verwendet. Die beiden vermischen sich kaum.
sbi
-3

Etwas stört mich an:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Erstens irritiert das Lesen des Wortes "Tauschen", wenn mein Verstand "Kopieren" denkt, meinen gesunden Menschenverstand. Außerdem stelle ich das Ziel dieses ausgefallenen Tricks in Frage. Ja, Ausnahmen beim Erstellen der neuen (kopierten) Ressourcen sollten vor dem Austausch auftreten. Dies scheint ein sicherer Weg zu sein, um sicherzustellen, dass alle neuen Daten gefüllt sind, bevor sie live geschaltet werden.

Das ist gut. Was ist also mit Ausnahmen, die nach dem Tausch auftreten? (Wenn die alten Ressourcen zerstört werden, wenn das temporäre Objekt den Gültigkeitsbereich verlässt.) Aus Sicht des Benutzers der Zuweisung ist die Operation fehlgeschlagen, außer dies war nicht der Fall. Es hat einen großen Nebeneffekt: Die Kopie ist tatsächlich passiert. Es war nur eine Ressourcenbereinigung, die fehlgeschlagen ist. Der Status des Zielobjekts wurde geändert, obwohl der Vorgang von außen fehlgeschlagen zu sein scheint.

Also schlage ich vor, anstelle von "Swap" einen natürlicheren "Transfer" durchzuführen:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Es gibt noch die Konstruktion des temporären Objekts, aber die nächste sofortige Aktion besteht darin, alle aktuellen Ressourcen des Ziels freizugeben, bevor die Ressourcen der Quelle dorthin verschoben werden (und NULL, damit sie nicht doppelt freigegeben werden).

Anstelle von {konstruieren, bewegen, zerstören} schlage ich {konstruieren, zerstören, bewegen} vor. Der Zug, der die gefährlichste Aktion ist, ist der letzte, der ausgeführt wird, nachdem alles andere erledigt ist.

Ja, ein Fehlschlag der Zerstörung ist in beiden Schemata ein Problem. Die Daten sind entweder beschädigt (kopiert, wenn Sie nicht dachten, dass dies der Fall ist) oder verloren (freigegeben, wenn Sie nicht glauben, dass dies der Fall ist). Verloren ist besser als korrupt. Keine Daten sind besser als schlechte Daten.

Übertragen statt tauschen. Das ist sowieso mein Vorschlag.

Matthew
quelle
2
Ein Destruktor darf nicht versagen, daher werden Ausnahmen bei der Zerstörung nicht erwartet. Und ich verstehe nicht, was der Vorteil wäre, den Zug hinter die Zerstörung zu bewegen, wenn der Zug die gefährlichste Operation ist? Das heißt, im Standardschema wird ein Verschiebungsfehler den alten Status nicht beschädigen, wohingegen Ihr neues Schema dies tut. Warum also? Außerdem First, reading the word "swap" when my mind is thinking "copy" irritates-> Als Bibliotheksschreiber kennen Sie normalerweise gängige Praktiken (Kopieren + Tauschen), und der springende Punkt ist my mind. Ihr Geist ist tatsächlich hinter der öffentlichen Schnittstelle verborgen. Darum geht es bei wiederverwendbarem Code.
Sebastian Mach