Operator=: Copy&Swap vs. einzelne Zuweisungen
-
@Nexus:
copy & swap ist toll wenn neben dem einfachen "member zuweisen" noch gewisse andere Dinge erfolgen müssen um das korrekte Funktionieren der Klasse zu gewährleisten.
z.B. bei Smart Pointern oder ähnlichem.
Ohne copy & swap muss man im operator = viel mehr Code schreiben, und vor allem auch Code duplizieren. Und man baut im Endeffekt mehr Fehler (mehr Code wo man mitdenken muss => mehr Fehler).Wenn du ein Beispiel sehen willst ... hier: http://www.boost.org/doc/libs/1_35_0/boost/intrusive_ptr.hpp
-
Shade Of Mine schrieb:
man darf std::swap nicht ueberladen
Ist es nicht gerade für diesen Zweck erlaubt, namespace std zu erweitern? Ich bin mir ziemlich sicher, dass das ein Irrtum ist, weiß aber auf anhieb auch nicht genau, wo man es nachlesen bzw. wie ich es belegen könnte. Spätestens wenn camper über diesen Beitrag stolptert, bekommen wir Antwort.

Edit: 17.4.3.1 sagt:
It is undefined for a C++ program to add declarations or definitions to namespace std or namespaces within namespace std unless otherwise specified. A program may add template specializations for any standard library template to namespace std. Such a specialization (complete or partial) of a standard library template results in undefined behavior unless the declaration depends on a user-defined name of external linkage and unless the specialization meets the standard library requirements for the original template. 163)
Und Fußnote 163:
Any library code that instantiates other library templates must be prepared to work adequately with any user-supplied specialization
that meets the minimum requirements of the Standard.Btw - zusätzliche völlig legitime Template-Parameter der Bibliothek sollten hier auch nicht querschlagen, da diese einen Default haben müssen, der bei der Spezialisierung dann einfach in Kraft tritt.
Also ShadeOfMine hat schon recht - überladen darf man nicht. Aber spezialisieren.
-
Die "traditionelle" (ich glaube eigentlich, die Mehrzahl der C++ Programmierer nutzt Copy&Swap, aber egal) ist definitiv nicht exception-safe. Es können bei _jeder_ Zuweisung eines nicht trivialen Objektes an ein anderes Zombie Objekte entstehen. Ziemlich unschön.
#include <iostream> #include <exception> class A { public: class A(int val) : val(val) { } int val; }; class B { public: void operator = (const B& other) { if (/* something */) throw std::exception("Error!"); } } class MyClass { public: MyClass(int val) : a(val), b() { } // Wenn b wirft, wird a zerstört. // Ein MyClass Objekt wird nicht erstellt. MyCLass(const MyClass& other) : a(other.a), b(other.b) { } void operator = (const MyClass& other) { // Traditionell: if (this != &other) { a = other.a; b = other.b; // Wenn das hier wirft, hast du ein Zombie Objekt, // da nur a zugewiesen wurde. } return *this; // Copy&Swap: MyClass temp(other); // Wenn hier geworfen wird, passiert nix. swap(*this, temp); // Swap wirft nie ein exception, // wenn man es korrekt implementiert. return *this; // Es entstehen nie Zombie Objekte. } private: A a; B b; } int main() { MyClass m1(2), m2(3); try { m1 = m2; } catch(...) { } // m1 ist jetzt möglicherweise ein Zombie Objekt, // dass nicht mehr verwendet werden darf, // bei Copy&Swap hingegen, hat es seinen gültigen // Startwert behalten. }
-
7H3 N4C3R schrieb:
Shade Of Mine schrieb:
man darf std::swap nicht ueberladen
Ist es nicht gerade für diesen Zweck erlaubt, namespace std zu erweitern?
Und in vielen Faellen muss man ueberladen weil spezialisierung nicht geht. du kannst zB keine funktion partiell spezialisieren, ergo jede template klasse kann kein std::swap verwenden...
der standard sollte einfach überladungen für udts erlauben und gut wäre es
-
Ich bin nicht so überzeugt, dass man dieses this!=&other überhaupt als traditionell ansehen kann. Die eigentliche Urform sieht ja zweifellos so aus:
Foo& operator=(const Foo& rhs) { Member1=rhs.Member1; Member2=rhs.Member2; Member3=rhs.Member3; return *this; }Das ist nicht ganz zufällig genau das, was der Compiler generiert. Das Ganze funktioniert natürlich sofort auch bei Selbstzuweisung, exceptionsicher ist es aber nicht. Nun schreiben wir den Operator i.d.R. nur dann selbst, wenn er anders aussehen muss, z.B. weil wir einen Zeiger haben, und eine tiefe Kopie brauchen.
Ich benutze mal folgendes Beispielstruct Foo { int* p; // Zeiger in Array size_t size; // Größe des Arrays ...Dann könnten wir schreiben
Foo& operator=(const Foo& rhs) { int* tmp = new int[ rhs.size ]; std::copy( rhs.p, rhs.p + rhs.size, tmp ); delete [] p; p = tmp; size = rhs.size; return *this; }Das ist - oh Wunder - sicher bzgl. Selbstzuweisung. Es ist sogar exceptionsicher, einfach deshalb, weil nur an einer einzigen Stelle eine Exception auftreten kann (wenn wir sinnvollerweise vorausetzen, dass der delete-Operator keine Exception werfen kann - das ist im Übrigen auch bei Copy&Swap notwendig), und vor dieser Stelle unser Objekt noch gar nicht verändert wurde.
Wozu brauchen wir dann den Test? Der wird erst notwendig, wenn wir - unsinnigerweise - versuchen, tmp loszuwerden.Foo& operator=(const Foo& rhs) { delete [] p; p = new int[ rhs.size ]; std::copy( rhs.p, rhs.p + rhs.size, p ); size = rhs.size; return *this; }tmp sind wir damit los. Die Sicherheit bei Selbstzuweisung und die Exceptionsicherheit allerdings auch. Warum würde man das überhaupt schreiben wollen? Entweder weil es scheinbar effizienter ist (ein Trugschluss), oder man schlicht zu schematisch denkt, dass Zuweisung im Grunde die Zerstörung des alten Inhalts+Kopieren des neuen Inhalts ist, also
Foo& operator=(const Foo& rhs) { this->~Foo(); new( this ) Foo( rhs ); return *this; }Das ist zwar nicht grundsätzlich falsch gedacht, aber die Reihenfolge ist schlicht verkehrt.
Copy&Swap sorgt für die richtige Reihenfolge und vermeidet unnötige Codeduplikation - grundsätzlich geht man davon aus, dass swap sowieso existiert und nicht nur um op= zu implementierten. Die bloße Existenz eines weiteren Objektes ebenso wie der bloße Aufruf zusätzlicher Funktionen bedeutet keine Ineffizienz, denn Funktionsaufrufe sind an sich nicht beobachtbar (man denke an inline). Problematisch kann immer nur das sein, was in diesen Funktionen passiert - dabei stellen wir fest, dass Ineffizienz eigentlich nur dann auftritt, wenn wir viele einfache skalare Objekte als Member haben: in diesem Falle können wir uns aber die eigene Implementation in der Regel sowieso sparen.
Es kommt hinzu, dass der Test if (this!=&rhs) so nur im ganz speziellen Fall des Zuweisungsoperators nützlich ist. Haben wir es mit anderen Operatoren zu tun (wie a+=a oder a*=a), können wir im Falle der Gleichheit nicht einfach nichts tun, was zu Code-Duplikation führen würde.Eine interessante und nützliche Eigenschaft hat der Test if(this!=&other) allerdings: er garantiert, das Selbstzuweisung nicht fehlschlagen kann, was im Prinzip eigentlich selbstverständlich sein sollte. Diesen Test zusätzlich durchzuführen (obwohl er ansonsten nicht notwendig ist), bedeutet aber oft einen inakzeptablen Overhead, da Selbstzuweisung einfach nur sehr selten auftritt. Auch dafür gibt es eine Lösung
Foo& operator=(const Foo& rhs) { try { Foo tmp( rhs ); tmp.swap( *this ); } catch ( ... ) { if ( this != &rhs ) throw; } return *this; }Allerdings ein bisschen viel für einen seltenen und praktisch wenig relevanten Fall.
Ist damit alles gesagt? Sicherlich nicht. Ein Problem mit Copy&Swap ist, dass es in Basisklassen nicht so recht funktionieren will. Betrachte
struct A { X x; // Zuweisung könnte fehlschlagen Y y; // Zuweisung könnte fehlschlagen virtual foo() = 0; virtual ~A() {} A& operator=(const A&) { .. was nun ?Einfaches Copy&Swap entfällt hier. Da A abstrakt ist, können wir nicht direkt eine Kopie erstellen. Man könnte eine lokale Klasse erstellen, die von A erbt und alle rein-virtuellen Funktion mit dummy-Funktionen überschreibt und so eine Kopie erstellen. Ich bin aber nicht überzeugt, dass das sehr elegant ist.
-
Vielen Dank an alle für die ausführlichen Antworten!
Die Thematik ist mir relativ neu, deshalb gibt es momentan noch viele Dinge, die ich nicht verstehe. Im Internet hab ich auf en.wikibooks.org noch einiges dazu gefunden, trotzdem leuchtet mir noch nicht alles ein.Don06 schrieb:
// Traditionell: if (this != &other) { a = other.a; b = other.b; // Wenn das hier wirft, hast du ein Zombie Objekt, // da nur a zugewiesen wurde. } return *this; // Copy&Swap: MyClass temp(other); // Wenn hier geworfen wird, passiert nix. swap(*this, temp); // Swap wirft nie ein exception, // wenn man es korrekt implementiert. return *this; // Es entstehen nie Zombie Objekte.@ Don06: Du sagst, beim "traditionellen" Verfahren könne eine Exception geworfen werden, bei Copy&Swap hingegen nicht. Ist die eigene
swap()-Funktion tatsächlich exceptionsicherer? Prinzipiell könnte ja trotzthrow()eine Ausnahme geworfen werden, und intern werden genauso Zuweisungen durchgeführt...@ camper: Wäre dein folgendes Beispiel dann nicht auch eine Möglichkeit, um ohne
swap()auszukommen? Und stattstd::copykönnte man elementweise Kopien manuell durchführen?Foo& operator=(const Foo& rhs) { int* tmp = new int[ rhs.size ]; std::copy( rhs.p, rhs.p + rhs.size, tmp ); delete [] p; p = tmp; size = rhs.size; return *this; }camper schrieb:
Entweder weil es scheinbar effizienter ist (ein Trugschluss)
Es macht vielleicht nicht viel aus, aber wenn mehr Funktionen aufgerufen werden, ist das von der Performance her generell langsamer... Klar verstehe ich, dass es sich wegen der Sicherheit nicht lohnt, ich meine ja nur

camper schrieb:
grundsätzlich geht man davon aus, dass swap sowieso existiert und nicht nur um op= zu implementierten.
Ja, aber wenn man ein Klassentemplate hat und dafür z.B. den Zuweisungsoperator überlädt, kann man sich ja nicht einfach darauf verlassen, dass für den Templatetypen eine Funktion
swap()existiert, zumal dieser auch ein elementarer Datentyp sein kann. Wie handhabt man es dann?
-
Nexus schrieb:
@ Don06: Du sagst, beim "traditionellen" Verfahren könne eine Exception geworfen werden, bei Copy&Swap hingegen nicht. Ist die eigene
swap()-Funktion tatsächlich exceptionsicherer? Prinzipiell könnte ja trotzthrow()eine Ausnahme geworfen werden, und intern werden genauso Zuweisungen durchgeführt...Natürlich kann man eine swap funktion schreiben die eine exception wirft, aber das ist wie ein dtor der exceptions wirft: einfach nur böse und dumm.
die ganze idee eines swap ist es, eine art "commit" zu haben, und commits dürfen nicht fehlschlagen.
es ist ja auch trivial swap auf nicht werfende operationen zu reduzieren, da man am ende der schlange ja nur builtins tauschen muss...
@ camper: Wäre dein folgendes Beispiel dann nicht auch eine Möglichkeit, um ohne
swap()auszukommen? Und stattstd::copykönnte man elementweise Kopien manuell durchführen?Foo& operator=(const Foo& rhs) { int* tmp = new int[ rhs.size ]; std::copy( rhs.p, rhs.p + rhs.size, tmp ); delete [] p; p = tmp; size = rhs.size; return *this; }ja, nur du hast ein memory leak

Foo& operator=(const Foo& rhs) { int* tmp = new int[ rhs.size ]; try { std::copy( rhs.p, rhs.p + rhs.size, tmp ); } catch(...) { delete [] tmp; throw; } delete [] p; p = tmp; size = rhs.size; return *this; }und desto mehr werfende aktionen du hast, desto komplexer und fehleranfälliger wird es. und du bist nicht mehr schneller als copy&swap, da du ja ebenfalls copy&swap machst, nur eben umständlicher... ok, du sparst dir die hälfte der movs im besten fall, aber wenn xchg oder so verwendet wird, dann sparst du dir garnix. also wir reden hier bereits ueber 1-2 einzelne cpu operationen als performance unterschied.
wobei das try ja auch nicht gratis ist...
Es macht vielleicht nicht viel aus, aber wenn mehr Funktionen aufgerufen werden, ist das von der Performance her generell langsamer... Klar verstehe ich, dass es sich wegen der Sicherheit nicht lohnt, ich meine ja nur

wenn du wegen den try/catch nicht sogar langsamer wirst... wir reden hier über sowenig einzelne operationen dass es absolut egal ist.
Ja, aber wenn man ein Klassentemplate hat und dafür z.B. den Zuweisungsoperator überlädt, kann man sich ja nicht einfach darauf verlassen, dass für den Templatetypen eine Funktion
swap()existiert, zumal dieser auch ein elementarer Datentyp sein kann. Wie handhabt man es dann?man ruft swap(a,b) auf und zwar eben unqualifiziert und mit einem using std::swap davor. jedevernünftige klasse definiert eine freie funktion swap dafür und für triviale klassen funktioniert der standardmäßige dreieckstausch von std::swap.
-
Danke für deine Antwort, Shade of Mine, das hat jetzt einiges geklärt.
Shade Of Mine schrieb:
Natürlich kann man eine swap funktion schreiben die eine exception wirft, aber das ist wie ein dtor der exceptions wirft: einfach nur böse und dumm.
Okay. Ich meinte nur, wenn man Klassen von anderen übernimmt und da eventuell stümperhaft gearbeitet wurde (d.h.
swap()wirft), aber dann hat man wahrscheinlich sowieso andere Probleme
Shade Of Mine schrieb:
und desto mehr werfende aktionen du hast, desto komplexer und fehleranfälliger wird es. und du bist nicht mehr schneller als copy&swap, da du ja ebenfalls copy&swap machst, nur eben umständlicher... ok, du sparst dir die hälfte der movs im besten fall, aber wenn xchg oder so verwendet wird, dann sparst du dir garnix. also wir reden hier bereits ueber 1-2 einzelne cpu operationen als performance unterschied.
Die Performance ist eigentlich auch nicht mein Hauptproblem, sondern ich sehe es als ziemlich grossen Aufwand an, in alle Klassen eine Swap-Funktion einzubauen, wenn das Gleiche auch ohne geht (vielleicht ein bisschen komplexer, aber dafür nur einmal). Aber ich hab natürlich noch nicht wahnsinnig viel Programmiererfahrung und kann das deshalb auch nicht sehr gut beurteilen - es ist einfach meine pragmatische Sichtweise.

Shade Of Mine schrieb:
man ruft swap(a,b) auf und zwar eben unqualifiziert und mit einem using std::swap davor. jedevernünftige klasse definiert eine freie funktion swap dafür und für triviale klassen funktioniert der standardmäßige dreieckstausch von std::swap.
Vielen Dank, jetzt hab ich das mit
usingendgültig verstanden; tut mir leid, wenn ich etwas schwer von Begriff war. Es scheint mir sehr praktisch, wenn automatisch entschieden wird, welcheswap()-Funktion verwendet wird.
-
Nexus schrieb:
Shade Of Mine schrieb:
Natürlich kann man eine swap funktion schreiben die eine exception wirft, aber das ist wie ein dtor der exceptions wirft: einfach nur böse und dumm.
Okay. Ich meinte nur, wenn man Klassen von anderen übernimmt und da eventuell stümperhaft gearbeitet wurde (d.h.
swap()wirft), aber dann hat man wahrscheinlich sowieso andere Probleme
Auch für übernommen Klassen, die diese Bedingungen nicht erfüllen, gibt es eine Lösung: http://www.gotw.ca/gotw/059.htm . Der Artikel sollte auch so interessant sein.
Gruß
Don06
-
Don06 schrieb:
Auch für übernommen Klassen, die diese Bedingungen nicht erfüllen, gibt es eine Lösung: http://www.gotw.ca/gotw/059.htm. Der Artikel sollte auch so interessant sein.
Die Seite geht leider nicht (HTTP 404: Nicht gefunden)...
-
@Nexus: Der hat nur den Punkt zu viel genommen.
-
Oh, ich hab wohl zu wenig genau geschaut

Danke für den Link, scheint interessant zu sein...
-
Nexus schrieb:
Die Performance ist eigentlich auch nicht mein Hauptproblem, sondern ich sehe es als ziemlich grossen Aufwand an, in alle Klassen eine Swap-Funktion einzubauen, wenn das Gleiche auch ohne geht (vielleicht ein bisschen komplexer, aber dafür nur einmal).
Du musst den Code für das swap so oder so in den op= rein schreiben.
dann kannst du es gleich in eine funktion packen und dort wiederverwenden.Bsp:
int* tmp = new int[ rhs.size ]; delete [] p; p = tmp; size = rhs.size;das wird zu:
void swap(T& a, T& b) { swap(a.size, b.size); swap(a.p, b.p); }der aufwand ist bei einem swap also deutlich weniger schon bei nur einer einzigen funktion... und swap muss man ja öfters aufrufen - eben immer wenn man transaktionen braucht. man kann es immer händisch machen, klar, aber eine eigene funktion dafür erleichtert eine menge - vorallem weil dann auch client code swap ausführen kann.
ein trick einen container komplett zu leeren und auch die capacity zu resetten kann man eben zB:
swap(c, vector<int>());
machen.etc.
swap hat viele anwendungsfälle...
zB um movable zu emulieren
-
Vielen Dank. So langsam beginnt mich
swap()auch zu überzeugen, aber ich muss mich wohl noch ausführlicher mit der Thematik beschäftigen...
-
Nexus schrieb:
Vielen Dank. So langsam beginnt mich
swap()auch zu überzeugen, aber ich muss mich wohl noch ausführlicher mit der Thematik beschäftigen...schau dir diesbezueglich auch gleich exception safety an. denn laufzeitfehler sind der hauptgrund warum man transaktionen braucht (denn wenn teile einer transaktion sowieso nie fehlschlagen, dann ist die transaktion ja irgendwie trivial ;))
und neben den genannten argumenten, ein:
T& operator=(T const& other) { T temp(other); swap(other); return *this; }ist einfach sexy

-
Okay, vielen Dank für die qualifizierte Hilfe!

-
Das Problem an Copy&Swap ist halt das Copy. Jedesmal ein temp Objekt zu erzeugen, muss nicht immer so gut sein, vorallem dann, wenn es nur eine begrenzte Anzahl von Objekten dieser Klasse geben darf. Da ist sowas besser
Foo& operator=(const Foo& rhs) { int* tmp = new int[ rhs.size ]; try { std::copy( rhs.p, rhs.p + rhs.size, tmp ); } catch(...) { delete [] tmp; throw; } delete [] p; p = tmp; size = rhs.size; return *this; }
-
Würdet ihr bei einer Klasse für Datenbankobjekte mit z.B. ID, Name und Adresse die ID beim Copycontruktor und bei der Zuweisung auch kopieren oder soll die immer eindeutig sein?
-
Shade Of Mine schrieb:
Warum schreibst du überhaupt Folgendes?
using std::swap;weil der c++ standard nicht perfekt ist. man darf std::swap nicht ueberladen und daher muss man swap im namespace der klasse definieren damit der koenig lookup zieht. deshalb darf man aber std::swap nicht schreiben, da ja uU foo::swap aufgerufen werden muss.
Kannst du das nochmal verständlicher schreiben? Was hat es mit dem Koenig-Lookup zu tun, dass man std::swap nicht überladen darf? Wieso muss evtl. foo::swap aufgerufen werden?
Meinst du den Fall, dass ich zwei Klassen in meiner Klasse habe - eine, die ein eigenes std::swap bietet und eine, die es nicht tut? Darüber wäre man sich bei der Implementierung seiner eigenen swap-Methode ja im klaren, dann könnte man immer noch std::swap vor Objekte ohne eigene swap-Methode nutzen, ansonsten eben das swap des jeweiligen Objektes.
Oder wie meinst du das? Bitte erkläre es mir nochmal.
-
datenbänker schrieb:
Würdet ihr bei einer Klasse für Datenbankobjekte mit z.B. ID, Name und Adresse die ID beim Copycontruktor und bei der Zuweisung auch kopieren oder soll die immer eindeutig sein?
Wenn du in C++ den copy-ctor implementierst, oder vom Compiler default implementieren lässt, dann ist es "üblich" dass die Klasse value-semantics hat. Und das heisst für mich ganz klar dass alle Werte 1:1 kopiert werden, also auch Dinge wie ein Primary-Key-Feld aus einer Tabelle.
Wenn das nicht mit einem bestimmten Design harmoniert, dann würde ich den copy-ctor gleich ganz sperren, und die Klasse damit unkopierbar machen.