Was ist diese Redewendung und wann sollte sie verwendet werden? Welche Probleme löst es? Ändert sich die Redewendung, wenn C ++ 11 verwendet wird?
Obwohl es an vielen Stellen erwähnt wurde, hatten wir keine singuläre Frage und Antwort "Was ist das?", Also hier ist es. Hier ist eine unvollständige Liste von Orten, an denen es zuvor erwähnt wurde:
Antworten:
Überblick
Warum brauchen wir die Copy-and-Swap-Sprache?
Jede Klasse, die eine Ressource verwaltet (ein Wrapper wie ein intelligenter Zeiger), muss The Big Three implementieren . Während die Ziele und die Implementierung des Kopierkonstruktors und des Destruktors unkompliziert sind, ist der Kopierzuweisungsoperator wohl der nuancierteste und schwierigste. Wie soll es gemacht werden? Welche Fallstricke müssen vermieden werden?
Das Copy-and-Swap-Idiom ist die Lösung und unterstützt den Zuweisungsoperator elegant dabei, zwei Dinge zu erreichen: Vermeidung von Codeduplizierungen und Bereitstellung einer starken Ausnahmegarantie .
Wie funktioniert es?
Konzeptionell wird die Funktionalität des Kopierkonstruktors verwendet, um eine lokale Kopie der Daten zu erstellen. Anschließend werden die kopierten Daten mit einer
swap
Funktion verwendet, wobei die alten Daten gegen die neuen Daten ausgetauscht werden. Die temporäre Kopie wird dann zerstört und nimmt die alten Daten mit. Wir erhalten eine Kopie der neuen Daten.Um das Copy-and-Swap-Idiom verwenden zu können, benötigen wir drei Dinge: einen funktionierenden Copy-Konstruktor, einen funktionierenden Destruktor (beide sind die Basis eines Wrappers und sollten daher sowieso vollständig sein) und eine
swap
Funktion.Eine Swap-Funktion ist eine nicht werfende Funktion, die zwei Objekte einer Klasse Mitglied für Mitglied austauscht. Wir könnten versucht sein, zu verwenden,
std::swap
anstatt unsere eigenen bereitzustellen, aber dies wäre unmöglich;std::swap
verwendet den Kopierkonstruktor und den Kopierzuweisungsoperator in seiner Implementierung, und wir würden letztendlich versuchen, den Zuweisungsoperator in Bezug auf sich selbst zu definieren!(Nicht nur das, sondern auch unqualifizierte Anrufe an
swap
verwenden unseren benutzerdefinierten Swap-Operator und überspringen die unnötige Konstruktion und Zerstörung unserer Klasse, diestd::swap
dies mit sich bringen würde.)Eine ausführliche Erklärung
Das Ziel
Betrachten wir einen konkreten Fall. Wir wollen in einer ansonsten nutzlosen Klasse ein dynamisches Array verwalten. Wir beginnen mit einem funktionierenden Konstruktor, Kopierkonstruktor und Destruktor:
Diese Klasse verwaltet das Array fast erfolgreich, muss jedoch
operator=
ordnungsgemäß funktionieren.Eine fehlgeschlagene Lösung
So könnte eine naive Implementierung aussehen:
Und wir sagen, wir sind fertig; Dies verwaltet jetzt ein Array ohne Lecks. Es gibt jedoch drei Probleme, die im Code nacheinander als gekennzeichnet sind
(n)
.Der erste ist der Selbstzuordnungstest. Diese Überprüfung dient zwei Zwecken: Sie verhindert auf einfache Weise, dass bei der Selbstzuweisung unnötiger Code ausgeführt wird, und schützt uns vor subtilen Fehlern (z. B. Löschen des Arrays, nur um zu versuchen, es zu kopieren). In allen anderen Fällen dient es lediglich dazu, das Programm zu verlangsamen und als Rauschen im Code zu wirken. Selbstzuweisung tritt selten auf, daher ist diese Prüfung meistens eine Verschwendung. Es wäre besser, wenn der Bediener ohne sie richtig arbeiten könnte.
Das zweite ist, dass es nur eine grundlegende Ausnahmegarantie bietet. Wenn dies
new int[mSize]
fehlschlägt, wurde*this
es geändert. (Die Größe ist nämlich falsch und die Daten sind weg!) Für eine starke Ausnahmegarantie müsste es sich um Folgendes handeln:Der Code wurde erweitert! Was uns zum dritten Problem führt: Codeduplizierung. Unser Zuweisungsoperator dupliziert effektiv den gesamten Code, den wir bereits an anderer Stelle geschrieben haben, und das ist eine schreckliche Sache.
In unserem Fall besteht der Kern nur aus zwei Zeilen (der Zuordnung und der Kopie), aber bei komplexeren Ressourcen kann dieses Aufblähen des Codes ein ziemlicher Aufwand sein. Wir sollten uns bemühen, uns niemals zu wiederholen.
(Man könnte sich fragen: Wenn so viel Code benötigt wird, um eine Ressource korrekt zu verwalten, was ist, wenn meine Klasse mehr als eine verwaltet? Dies scheint zwar ein berechtigtes Problem zu sein, erfordert jedoch nicht triviale
try
/catch
Klauseln, ist dies jedoch nicht -ausgabe. Das liegt daran, dass eine Klasse nur eine Ressource verwalten sollte !)Eine erfolgreiche Lösung
Wie bereits erwähnt, behebt das Copy-and-Swap-Idiom alle diese Probleme. Aber im Moment haben wir alle Anforderungen außer einer: eine
swap
Funktion. Während die Dreierregel erfolgreich die Existenz unseres Kopierkonstruktors, Zuweisungsoperators und Destruktors beinhaltet, sollte sie eigentlich "Die großen Dreieinhalb" heißen: Jedes Mal, wenn Ihre Klasse eine Ressource verwaltet, ist es auch sinnvoll, eineswap
Funktion bereitzustellen .Wir müssen unserer Klasse Swap-Funktionen hinzufügen, und das tun wir wie folgt: †:
( Hier ist die Erklärung warum
public friend swap
.) Jetzt können wir nicht nur unsere tauschendumb_array
, sondern Swaps im Allgemeinen können effizienter sein; Es werden lediglich Zeiger und Größen ausgetauscht, anstatt ganze Arrays zuzuweisen und zu kopieren. Abgesehen von diesem Bonus an Funktionalität und Effizienz sind wir jetzt bereit, die Copy-and-Swap-Sprache zu implementieren.Unser Auftragsoperator ist ohne weiteres:
Und das ist es! Mit einem Schlag werden alle drei Probleme auf einmal elegant angegangen.
Warum funktioniert es?
Wir bemerken zuerst eine wichtige Wahl: Das Parameterargument wird als Wert genommen . Man könnte zwar genauso gut Folgendes tun (und tatsächlich tun es viele naive Implementierungen der Redewendung):
Wir verlieren eine wichtige Optimierungsmöglichkeit . Darüber hinaus ist diese Auswahl in C ++ 11 von entscheidender Bedeutung, das später erläutert wird. (Im Allgemeinen lautet eine bemerkenswert nützliche Richtlinie wie folgt: Wenn Sie eine Kopie von etwas in einer Funktion erstellen möchten, lassen Sie den Compiler dies in der Parameterliste tun. ‡)
In beiden Fällen ist diese Methode zum Abrufen unserer Ressource der Schlüssel zur Vermeidung von Codeduplizierungen: Wir können den Code aus dem Kopierkonstruktor verwenden, um die Kopie zu erstellen, und müssen kein bisschen davon wiederholen. Nachdem die Kopie erstellt wurde, können wir sie austauschen.
Beachten Sie, dass beim Aufrufen der Funktion alle neuen Daten bereits zugewiesen, kopiert und zur Verwendung bereit sind. Dies gibt uns eine starke kostenlose Ausnahmegarantie: Wir werden die Funktion nicht einmal aufrufen, wenn die Erstellung der Kopie fehlschlägt, und es ist daher nicht möglich, den Status von zu ändern
*this
. (Was wir zuvor für eine starke Ausnahmegarantie manuell gemacht haben, macht der Compiler jetzt für uns; wie nett.)Zu diesem Zeitpunkt sind wir frei zu Hause, weil wir
swap
nicht werfen. Wir tauschen unsere aktuellen Daten gegen die kopierten Daten aus, um unseren Status sicher zu ändern, und die alten Daten werden temporär gespeichert. Die alten Daten werden dann freigegeben, wenn die Funktion zurückkehrt. (Wobei der Gültigkeitsbereich des Parameters endet und sein Destruktor aufgerufen wird.)Da die Redewendung keinen Code wiederholt, können wir keine Fehler im Operator einführen. Beachten Sie, dass dies bedeutet, dass wir keine Selbstzuweisungsprüfung mehr benötigen, um eine einheitliche Implementierung von zu ermöglichen
operator=
. (Außerdem haben wir keine Leistungseinbußen mehr bei Nicht-Selbstzuweisungen.)Und das ist die Copy-and-Swap-Sprache.
Was ist mit C ++ 11?
Die nächste Version von C ++, C ++ 11, enthält eine sehr wichtige Änderung bei der Verwaltung von Ressourcen: Die Dreierregel ist jetzt die Viererregel (anderthalb). Warum? Weil wir nicht nur in der Lage sein müssen, unsere Ressource zu kopieren und zu konstruieren, sondern sie auch verschieben und konstruieren müssen .
Zum Glück ist das ganz einfach:
Was ist denn hier los? Erinnern Sie sich an das Ziel der Bewegungskonstruktion: die Ressourcen einer anderen Instanz der Klasse zu entnehmen und sie in einem Zustand zu belassen, der garantiert zuweisbar und zerstörbar ist.
Was wir also getan haben, ist einfach: Initialisieren Sie über den Standardkonstruktor (eine C ++ 11-Funktion) und tauschen Sie dann mit aus
other
. Wir wissen, dass eine standardmäßig erstellte Instanz unserer Klasse sicher zugewiesen und zerstört werden kann, sodass wir wissen, dass wirother
nach dem Austausch dasselbe tun können.(Beachten Sie, dass einige Compiler die Konstruktordelegierung nicht unterstützen. In diesem Fall müssen wir die Klasse standardmäßig manuell erstellen. Dies ist eine unglückliche, aber glücklicherweise triviale Aufgabe.)
Warum funktioniert das?
Das ist die einzige Änderung, die wir an unserer Klasse vornehmen müssen. Warum funktioniert das? Denken Sie an die immer wichtige Entscheidung, den Parameter zu einem Wert und nicht zu einer Referenz zu machen:
Wenn
other
jetzt mit einem r-Wert initialisiert wird, wird er verschiebungskonstruiert . Perfekt. Auf die gleiche Weise, wie wir in C ++ 03 unsere Kopierkonstruktorfunktionalität wiederverwenden können, indem wir das Argument als Wert verwenden, wählt C ++ 11 bei Bedarf auch automatisch den Verschiebungskonstruktor aus. (Und natürlich kann, wie in dem zuvor verlinkten Artikel erwähnt, das Kopieren / Verschieben des Werts einfach ganz weggelassen werden.)Und so schließt die Copy-and-Swap-Sprache.
Fußnoten
* Warum setzen wir
mArray
auf null? Denn wenn ein weiterer Code im Operator ausgelöst wird, wirddumb_array
möglicherweise der Destruktor von aufgerufen. und wenn dies geschieht, ohne es auf null zu setzen, versuchen wir, bereits gelöschten Speicher zu löschen! Wir vermeiden dies, indem wir es auf null setzen, da das Löschen von null keine Operation ist.† Es gibt andere Behauptungen, dass wir uns auf
std::swap
unseren Typ spezialisieren, eine Klasseswap
neben einer freien Funktion bereitstellenswap
sollten usw. Dies ist jedoch alles unnötig: Jede ordnungsgemäße Verwendungswap
erfolgt durch einen unqualifizierten Anruf, und unsere Funktion wird es sein gefunden durch ADL . Eine Funktion reicht aus.‡ Der Grund ist einfach: Sobald Sie die Ressource für sich haben, können Sie sie austauschen und / oder verschieben (C ++ 11), wo immer sie sein muss. Durch Erstellen der Kopie in der Parameterliste maximieren Sie die Optimierung.
†† Der Verschiebungskonstruktor sollte im Allgemeinen sein
noexcept
, andernfallsstd::vector
wird der Kopierkonstruktor von Code (z. B. Größenänderungslogik) verwendet, auch wenn eine Verschiebung sinnvoll wäre. Markieren Sie es natürlich nur dann als nicht, wenn der darin enthaltene Code keine Ausnahmen auslöst.quelle
swap
während der ADL gefunden werden, wenn Sie möchten, dass es in den meisten generischen Codes funktioniert, auf die Sie stoßen, wie inboost::swap
anderen Swap-Instanzen. Swap ist ein heikles Problem in C ++, und im Allgemeinen sind wir uns alle einig, dass ein einzelner Zugriffspunkt (aus Gründen der Konsistenz) am besten ist. Der einzige Weg, dies im Allgemeinen zu tun, ist eine kostenlose Funktion (int
kann kein Swap-Mitglied haben, zum Beispiel). Siehe meine Frage für einige Hintergrundinformationen.Die Zuweisung besteht im Kern aus zwei Schritten: Abreißen des alten Zustands des Objekts und Erstellen seines neuen Zustands als Kopie des Zustands eines anderen Objekts.
Im Grunde ist es das, was der Destruktor und der Kopierkonstruktor tun. Die erste Idee wäre also, die Arbeit an sie zu delegieren. Da die Zerstörung jedoch nicht scheitern darf, während die Konstruktion dies könnte, möchten wir es tatsächlich umgekehrt machen : Führen Sie zuerst den konstruktiven Teil aus und, falls dies erfolgreich war, dann den destruktiven Teil . Das Copy-and-Swap-Idiom ist eine Möglichkeit, genau das zu tun: Es ruft zuerst den Kopierkonstruktor einer Klasse auf, um ein temporäres Objekt zu erstellen, tauscht dann seine Daten mit den temporären aus und lässt dann den Destruktor des temporären Objekts den alten Zustand zerstören.
Schon seit
swap()
soll niemals scheitern, der einzige Teil, der scheitern könnte, ist die Kopierkonstruktion. Dies wird zuerst ausgeführt, und wenn dies fehlschlägt, wird im Zielobjekt nichts geändert.In seiner verfeinerten Form wird Copy-and-Swap implementiert, indem die Kopie durch Initialisieren des (Nichtreferenz-) Parameters des Zuweisungsoperators ausgeführt wird:
quelle
std::swap(this_string, that)
bietet keine No-Throw-Garantie. Es bietet starke Ausnahmesicherheit, aber keine No-Throw-Garantie.std::string::swap
(die von aufgerufen werdenstd::swap
) ausgelöst werden . In C ++ 0xstd::string::swap
istnoexcept
und darf keine Ausnahme ausgelöst werden .std::array
...)Es gibt bereits einige gute Antworten. Ich werde mich hauptsächlich auf das konzentrieren, was mir meiner Meinung nach fehlt - eine Erklärung der "Nachteile" mit der Copy-and-Swap-Sprache ....
Eine Möglichkeit, den Zuweisungsoperator in Form einer Swap-Funktion zu implementieren:
Die Grundidee ist:
Der fehleranfälligste Teil beim Zuweisen zu einem Objekt besteht darin, sicherzustellen, dass alle Ressourcen, die der neue Status benötigt, erfasst werden (z. B. Speicher, Deskriptoren).
Diese Erfassung kann versucht werden, bevor der aktuelle Status des Objekts (dh
*this
) geändert wird, wenn eine Kopie des neuen Werts erstellt wird, weshalb der Wert eher als Wert (dh als Kopie) als als Referenzrhs
akzeptiert wirdden Zustand der lokalen Kopie Swapping
rhs
und*this
ist in der Regel relativ einfach zu tun , ohne potenzielle Fehler / Ausnahmen, da die lokale Kopie danach fit (muss nur Zustand für die destructor Lauf benötigt keine besonderen Zustand, so wie für ein Objekt wird verschoben von in> = C ++ 11)Wenn Sie möchten, dass das zugewiesene Objekt von einer Zuweisung, die eine Ausnahme auslöst, nicht betroffen ist, vorausgesetzt, Sie haben oder können eine
swap
Garantie mit starker Ausnahme schreiben , und im Idealfall eine, die nicht fehlschlagen kann /throw
.. †Wenn Sie eine saubere, leicht verständliche und robuste Methode zum Definieren des Zuweisungsoperators in Bezug auf (einfachere) Kopierkonstruktor-
swap
und Destruktorfunktionen wünschen .†
swap
Werfen: Es ist im Allgemeinen möglich, Datenelemente, die die Objekte nach Zeigern verfolgen, zuverlässig auszutauschen, aber Nicht-Zeiger-Datenelemente, die keinen auswurffreien Austausch haben oder für die das Austauschen alsX tmp = lhs; lhs = rhs; rhs = tmp;
Kopierkonstruktion oder -zuweisung implementiert werden muss kann werfen, haben immer noch das Potenzial zu scheitern, einige Datenmitglieder ausgetauscht zu lassen und andere nicht. Dieses Potenzial gilt sogar für C ++ 03std::string
, da James eine andere Antwort kommentiert:‡ Die Implementierung eines Zuweisungsoperators, die beim Zuweisen von einem bestimmten Objekt aus vernünftig erscheint, kann bei der Selbstzuweisung leicht fehlschlagen. Während es unvorstellbar erscheint, dass Client-Code sogar versucht, sich selbst zuzuweisen, kann dies bei Algo-Operationen an Containern relativ leicht vorkommen, wobei
x = f(x);
Codef
(möglicherweise nur für einige#ifdef
Zweige) ein Makro-Ala#define f(x) x
oder eine Funktion ist, die einen Verweis aufx
oder sogar zurückgibt (wahrscheinlich ineffizient, aber prägnant) Code wiex = c1 ? x * 2 : c2 ? x / 2 : x;
). Zum Beispiel:Auf Selbstzuordnung, der obige Code löschen ist
x.p_;
, Punktep_
in einer neu Haufen Region zugewiesen, dann die zu lesen versucht uninitialised darin Daten (undefiniertes Verhalten), wenn das nicht etwas zu seltsam tut,copy
versucht , eine Selbstzuordnung zu jedem nur- zerstörtes 'T'!⁂ Das Copy-and-Swap-Idiom kann aufgrund der Verwendung eines zusätzlichen temporären Systems zu Ineffizienzen oder Einschränkungen führen (wenn der Parameter des Bedieners kopierkonstruiert ist):
Hier
Client::operator=
könnte eine Handschrift prüfen, ob sie*this
bereits mit demselben Server verbunden ist wierhs
(möglicherweise wird ein "Reset" -Code gesendet, falls dies nützlich ist), während der Copy-and-Swap-Ansatz den Copy-Konstruktor aufruft, der wahrscheinlich zum Öffnen geschrieben wird eine eindeutige Socket-Verbindung schließen Sie dann die ursprüngliche. Dies könnte nicht nur eine Remote-Netzwerkinteraktion anstelle einer einfachen in Bearbeitung befindlichen Variablenkopie bedeuten, sondern auch die Client- oder Serverbeschränkungen für Socket-Ressourcen oder -Verbindungen verletzen. (Natürlich hat diese Klasse eine ziemlich schreckliche Oberfläche, aber das ist eine andere Sache ;-P).quelle
Client
ist, dass die Zuweisung nicht verboten ist.Diese Antwort ist eher eine Ergänzung und eine geringfügige Änderung der obigen Antworten.
In einigen Versionen von Visual Studio (und möglicherweise anderen Compilern) gibt es einen Fehler, der wirklich ärgerlich und nicht sinnvoll ist. Wenn Sie also Ihre
swap
Funktion wie folgt deklarieren / definieren :... der Compiler schreit Sie an, wenn Sie die
swap
Funktion aufrufen :Dies hat etwas damit zu tun, dass eine
friend
Funktion aufgerufen und einthis
Objekt als Parameter übergeben wird.Eine Möglichkeit, dies zu
friend
umgehen, besteht darin, kein Schlüsselwort zu verwenden und dieswap
Funktion neu zu definieren :Dieses Mal können Sie einfach aufrufen
swap
und übergebenother
, was den Compiler glücklich macht:Immerhin müssen Sie nicht brauchen eine verwenden
friend
Funktion Swap - 2 - Objekte. Genauso sinnvoll ist es,swap
eine Member-Funktion zuother
erstellen , die ein Objekt als Parameter hat.Sie haben bereits Zugriff auf das
this
Objekt, daher ist die Übergabe als Parameter technisch überflüssig.quelle
friend
Funktion mit dem*this
ParameterIch möchte ein Wort der Warnung hinzufügen, wenn Sie sich mit Allokator-fähigen Containern im C ++ 11-Stil befassen. Swapping und Assignment haben eine subtil unterschiedliche Semantik.
Betrachten wir der Vollständigkeit halber einen Container
std::vector<T, A>
, bei demA
es sich um einen Stateful Allocator-Typ handelt, und vergleichen Sie die folgenden Funktionen:Der Zweck beider Funktionen
fs
undfm
ist es,a
den Zustand zu geben ,b
der ursprünglich hatte. Es gibt jedoch eine versteckte Frage: Was passiert, wenna.get_allocator() != b.get_allocator()
? Die Antwort lautet: Es kommt darauf an. Lass uns schreibenAT = std::allocator_traits<A>
.Wenn dies der Fall
AT::propagate_on_container_move_assignment
iststd::true_type
, wirdfm
der Allokator vona
mit dem Wert von neu zugewiesenb.get_allocator()
, andernfalls wird dies nicht der Fall sein, unda
der ursprüngliche Allokator wird weiterhin verwendet. In diesem Fall müssen die Datenelemente einzeln ausgetauscht werden, da die Speicherung vona
undb
nicht kompatibel ist.Wenn dies der Fall
AT::propagate_on_container_swap
iststd::true_type
, werdenfs
sowohl Daten als auch Allokatoren in der erwarteten Weise ausgetauscht.Wenn
AT::propagate_on_container_swap
jastd::false_type
, dann brauchen wir eine dynamische Prüfung.a.get_allocator() == b.get_allocator()
, verwenden die beiden Container kompatiblen Speicher, und der Austausch erfolgt auf die übliche Weise.a.get_allocator() != b.get_allocator()
, hat das Programm ein undefiniertes Verhalten (vgl. [Container.requirements.general / 8].Das Ergebnis ist, dass das Austauschen in C ++ 11 zu einer nicht trivialen Operation geworden ist, sobald Ihr Container Stateful Allocators unterstützt. Dies ist ein etwas "fortgeschrittener Anwendungsfall", aber nicht ganz unwahrscheinlich, da Verschiebungsoptimierungen normalerweise erst dann interessant werden, wenn Ihre Klasse eine Ressource verwaltet und der Speicher eine der beliebtesten Ressourcen ist.
quelle