Operator=: Copy&Swap vs. einzelne Zuweisungen
-
@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.
-
Die drei ??? schrieb:
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?
mit koenig lookup hat das erstmal nix zu tun. der standard sagt lediglich, dass man in den namensraum std nur template spezialisierungen setzen darf, aber keine ueberladungen.
waeren ueberladungen erlaubt, waere das thema erledigt.
da aber ueberladungen nicht erlaubt sind, muss man sich fuer template klassen etwas anderes ueberlegen. denn swap fuer eine templateklasse spezialisieren geht nicht, da man dazu ja partielle spezialisierung braucht und das bei funktionen nicht geht.
man muss die swap funktion nun im namespace der klasse neu definieren. hier kommt koenig lookup dazu, oder klarer formuliert: adl - argument dependent lookup. adl sagt: suche nicht nur im aktuellen namensraum nach einer passenden funktion, sondern in allen namensraeumen aller parameter der funktion ebenfalls.
deshalb kann ein swap() gefunden werden, wenn es im namensraum der klasse liegt.
es ist deshalb wichtig eine swap funktion anzubieten, da ein swap member nicht immer moeglich ist. bsp: builtins. int hat definitiv keine swap memberfunktion.
wie soll eine template funktion aber nun ein objekt von ihrem template parameter swapen koennen? das geht eben ueber die freie funktion swap. denn nur so koennen wir fuer alle typen ein swap verwenden.
wenn wir aber nun std::swap aufrufen, umgehen wir den adl da wir explizit sagen welches swap wir wollen indem wir voll qualifizieren. wir muessen deshalb
using std::swap;
swap(a,b);machen. das using um std::swap sichtbar zu machen und das unqualifizierte swap fuer ADL. wenn wir ueber ADL keine passende funktion finden, weil a und b zB int sind, dann wird std::swap gefunden.
und das alles koennten wir uns ersparen, wenn man funktion mit UDTs im namespace std ueberladen duerfte.
-
Die drei ??? schrieb:
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.
1. swap darf im Namensraum std nicht zusätzlich überladen werden.
2. Spezialisierungen von std::swap sind möglich, da es sich aber um ein Funktionstemplate handelt, gilt dies nur für explizite Spezialisierungen.
3. Für ein Klassentemplate ist es daher nicht möglich eine Spezialisierung von std::swap für alle möglichen Instantiierungen anzubieten.
4. Der Ausweg besteht darin, swap unqualifiziert zu lassen, und damit auf ADNL zu vertrauen. Man kann ein swap in einem assozierten Namensraum einer Klasse anbieten, dieses wird dann benutzt werden, weil es spezieller als std::swap ist.
5. Diese Möglichkeit besteht auch für Klassentemplates.
6. Ein 2. Template kann für seine Typparameter nicht feststellen, ob swap für diese Parameter in deren assozierten Namensraum überladen wurde oder nicht. Will man also von diesem 2. Template aus swap für diesen Templateparameter benutzen, muss man folglich std::swap in den Scope bringen, damit dieses gefunden und aufgerufen wird, falls es kein spezielleres swap für den Typparameter gibt. std::swap wird ja in aller Regel gerade nicht per ADNL gefunden werden, es sei denn, std ist zufällig mal assozierter Namensraum.
Code ist einfacher zu verstehen:template<typename T> class Foo { T x; void swap(Foo& other) // diese Form ist nötig, wenn man mit temporären Objekten Arbeiten will { // std::swap( x, other.x ); // findet keine spezielleren Überladungen von swap (außer denen, die zur Standardbibliothek gehören) // swap( x, other.x ); // findet kein swap für built-ins oder Klassen, die keine eigene Überladung anbieten using namespace std; // zwingend und unproblematisch (nur #include <algorithm> ist ZWINGEND erforderlich im gleichen Header // Anm. using std::swap wäre möglich, wenn wir zuvor alle Std-Header inkludieren die eine Überladung von swap anbieten // (nachfolgendes Inkludieren hilft nicht, da std::swap kein abhängiger Name ist) // die üblichen Regeln für kein using in Headern gelten NICHT für lokale Deklarationen bzgl. des Namensraums std // warum ? // weil: jede Standardkomponente, die ein spezialisiertes swap anbietet, dies automatisch im gleichen Header tut, der diese Komponente deklariert // zudem ist es VERBOTEN, für solche Komponenten swap explizit zu spezialisieren - sofern kein eigener UDT involviert ist // damit sind dem allgemeinen Verbot - kein using in Headern - zugrundeliegenden Probleme umschifft // 1. da es im lokalen Scope ist, hat es keine Auswirkungen auf den Rest des Programmes // 2. wegen der angesprochenen Beschränkungen hinsichtlich der Deklaration von Spezialisierungen gibt es kein Problem mit der Reihenfolge // ein Instantiierung des Aufrufs zu irgendeinem Zeitpunkt kann prinzipiell nicht erfolgen, bevor alle relevanten Deklarationen bekannt sind. // Die Bedeutung des swaps kann sich also im Nachhinein oder in anderen ÜEs nicht ändern, was undefiniert ist, und bei using in Headern allgemein ein Problem darstellt. swap( x, other.x ); // so geht es } friend void swap(Foo& lhs, Foo& rhs) // diese Form ist unsere benötigte Überladung im assozierten Namensraum { lhs.swap( rhs ); }
-
Shade Of Mine schrieb:
und neben den genannten argumenten, ein:
T& operator=(T const& other) { T temp(other); swap(other); return *this; }ist einfach sexy

Ja, nur: wieso die const-ref, wenn dann eh kopiert wird?
T& operator =(T other) { swap(other); return *this; }
-
Konrad Rudolph schrieb:
Ja, nur: wieso die const-ref, wenn dann eh kopiert wird?
konsitenz. man kann direkt kopieren, ich mach die kopie aber lieber intern, da ich sonst ja die implementierung nach aussen publiziere. schlimm ists nicht, aber imho ein schoenheitsfehler.
-
Shade Of Mine schrieb:
T& operator=(T const& other) { T temp(other); swap(other); return *this; }Du meinst sicherlich swap(temp), oder?

-
Fellhuhn schrieb:
Shade Of Mine schrieb:
T& operator=(T const& other) { T temp(other); swap(other); return *this; }Du meinst sicherlich swap(temp), oder?

natürlich
-
Shade Of Mine schrieb:
Konrad Rudolph schrieb:
Ja, nur: wieso die const-ref, wenn dann eh kopiert wird?
konsitenz. man kann direkt kopieren, ich mach die kopie aber lieber intern, da ich sonst ja die implementierung nach aussen publiziere. schlimm ists nicht, aber imho ein schoenheitsfehler.
Andererseits kann es dank RVO effizienter sein. Betrachte
T a,b,c; c = a + b;Erstellen wir die Kopie des temporären Objektes a+b erst im operator=, so kann diese Operation nicht eliminiert werden. Anders dagegen, wenn wir einen Value-Parameter benutzen (dieser Fall ist fast schon so gut wie richtiges move). Bei copy&swap erscheint es ohnehin sinnvoll, den Operator inline zu definieren - die interessanten Funktionen, die sich ggf. im Laufen der Zeit ändern, sind ja nur der copy-ctor und das swap. Dann ist allerdings die Frage der Konsistenz kein besonders gravierendes Argument.
-
camper schrieb:
Dann ist allerdings die Frage der Konsistenz kein besonders gravierendes Argument.
Können Compiler das wirklich nicht optimieren? Muss ich mir mal näher ansehen, hätte da eigentlich für beide varianten mit dem selben produzierten code gerechnet...