Operator=: Copy&Swap vs. einzelne Zuweisungen



  • 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 trotz throw() 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 statt std::copy kö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 using endgültig verstanden; tut mir leid, wenn ich etwas schwer von Begriff war. Es scheint mir sehr praktisch, wenn automatisch entschieden wird, welche swap() -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.



  • 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.


  • Mod

    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


Anmelden zum Antworten