Wie swap in eigener Klasse machen?


  • Mod

    Benutzernamer schrieb:

    using std::swap;
            swap(lhs.v, rhs.v);
    

    using heißt: nächste wird auf jeden fall std:swap genommen. Da v Vektoren sind, was macht das für einen Unterschied, ob ich std::swap oder using std::swap schreibe?

    In der Regel wird auch std::swap funktionieren, es gibt aber (theoretische - wer so was macht, will Probleme bekommen) Fälle, für die die beiden Varianten nicht die gleiche Funktion aufrufen:

    #include <vector>
    struct foo {};
    void swap(std::vector<foo>&, std::vector<foo>&); // (1)
    
    int main() {
        vector<foo> a,b;
        using std::swap;
        swap(a,b);      // ruft (1) auf
        std::swap(a,b); // ruft swap im Namensraum std auf
    }
    

    Überzeugender ist das, wenn du nicht mit std::vector anfängst, sondern mit irgendeinem fundamentalem Datentyp, wie z.B. int. Der unqualifizierte Aufruf von swap mit int funktioniert nur, wenn dieses vorher per using sichtbar gemacht wurde. Hast du aber auf using verzichtet und gleich std::swap aufgerufen, musst du diesen Code später ändern, falls du bei späterer Programmwartung den Typ änderst auf einen anderen (nicht-std) Typen, der ein spezialisiertes swap hat.

    Du kannst sogar in vielen Fällen einfach nur

    swap(lhs.v, rhs.v);
    

    schreiben, wenn du weist, dass die Argumenttypen ein eigenes swap haben oder aus dem Namensraum std stammen (vorausgesetzt, du bist kein Schlitzohr, und hast nicht irgendwo, wo Namelookup es finden kann, noch ein swap deklarariert, dass keine Funktion und kein Funktionstemplate - also z.B. eine Klasse - ist).

    using std::swap;
    

    wird essentiell, wenn die Argumenttypen noch nicht kennst - also vor allem in Templates.
    using std::swap garantiert in diesen Fällen, dass
    1. das gewöhnliche std::swap gefunden werden kann für Typen, die keigenes swap mitbringen, und
    2. irgendwelche Guerilla-Deklarationen von swap, die keine Funktionen sind, verdeckt werden (und diese somit keinen Schaden anrichtem, indem sie ADL verhindern).

    Meine Empfehlung:
    immer using std::swap + unqualifizierten Aufruf von swap.
    Das funktioniert immer und ist idiomatisch - man muss sich also nicht jedesmal neu darüber Gedanken machen, ob es so richtig ist.


  • Mod

    Benutzernamer schrieb:

    :A(std::move(other)
    

    Das kannte ich noch nicht. Und das wär meine Frage gewesen.
    Sehr nützliche information. Danke!
    Und das muss zwingend nach dem ":" stehen? Wenn es da nicht steht wird der default Konstruktor von A verwendet?

    Jedes B-Objekt enthält ein A-Subobjekt. Wenn also ein B-Objekt erzeugt wird, muss auf irgendeine Weise auch der A-Anteil darin konstruiert werden. Und das geschieht auf natürliche Weise durch einen Konstruktor (dafür sind sie da). Wird kein Konstruktor angegeben, bleibt es defaultmäßig beim Default-Konstruktor...
    Auch der Copy-ctor sollte logischerweise entsprechend den zugehörigen Konstruktor der Basisklasse aufrufen.
    In der Praxis sieht es dann noch ein bisschen anders aus: Es ist eher ungewöhnlich, dass es in mehreren Vererbungsebenen notwendig ist, entsprechend der Regel der Fünf jeweils eigene Copy-/Movectors/Zuweisungsops/Destruktoren zu implementieren. Das deutet auf schlechtes Design hin. In der Regel wird das nur in Ebene erforderlich sein und für alles andere kann man es dem Compiler überlassen, die Implementierungen zu liefern. Diese defaults machen dann bereits das richtige DingTM.



  • Ah, ok das macht Sinn. Aber eine Frage habe ich noch 🙂

    camper schrieb:

    Du kannst sogar in vielen Fällen einfach nur

    swap(lhs.v, rhs.v);
    

    schreiben, wenn du weist, dass die Argumenttypen ein eigenes swap haben oder aus dem Namensraum std stammen

    Wenn ich nun noch eine swap Funktion mit nur einem Argument habe darf ich das using std::swap nicht weglassen, obwohl es für Vektoren eigentlich definiert sein sollte.

    #include <algorithm>
    #include <array>
    #include <iostream>
    #include <vector>
    
    //(normal in einem header)
    class A {
      public:
        const int size = 9001;
        std::vector<int> v;
        A();
        A(const A &a);
        A(A &&a) ;
    
        A &operator=(const A &a) ;
        A &operator=(A &&a) ;
        friend void swap(A &lhs, A &rhs) ;
        void swap(A &lhs);
    };
    
    //(auch in den header? aufteilen?)
    namespace std {
        template <>
        void swap(A& a, A& b)
        {
            std::cout<< "swap" << std::endl; 
            swap(a, b); 
        }
    }
    //(in der .cpp:)
        A::A() : v(size) { std::cout << "cons" << std::endl; }
        A::A(const A &a) : v(a.v) { std::cout << "copy" << std::endl; }
        A::A(A &&a) : v(std::move(a.v)) { std::cout << "move" << std::endl; }
    
        A& A::operator=(const A &a) {
            std::cout << "=" << std::endl;
            v = a.v;
            return *this;
        }
        A& A::operator=(A &&a) {
            std::cout << "move =" << std::endl;
            v = std::move(a.v);
            return *this;
        }
        void swap(A &lhs, A &rhs) {  //ist das die friend Funktion?
            std::cout << "my custon swap\n";
            using std::swap;
            swap(lhs.v, rhs.v);
        }
       void A::swap(A &lhs) {
            std::cout << "my custon swap2\n";
            using std::swap;     //hier kann ich es nicht wegmachen
            swap(v, lhs.v);
        }
    
    int main() {
        std::array<A, 17> va;
        std::cout << "Sortiere\n";
        va.back().v[0] = -10;
        std::sort(begin(va), end(va),
                  [](const auto &a1, const auto &a2) { return a1.v[0] < a2.v[0]; }); 
        std::cout << va[0].v[0] << '\n';
        std::swap(va[1],va[2]);
    }
    


  • auch und müsste ich dann nicht auch das using bei anderen Funktionen verwenden? z.b. std::move, std::sort, std::cout, ...


  • Mod

    Benutzernamer schrieb:

    Ah, ok das macht Sinn. Aber eine Frage habe ich noch 🙂

    camper schrieb:

    Du kannst sogar in vielen Fällen einfach nur

    swap(lhs.v, rhs.v);
    

    schreiben, wenn du weist, dass die Argumenttypen ein eigenes swap haben oder aus dem Namensraum std stammen

    Wenn ich nun noch eine swap Funktion mit nur einem Argument habe darf ich das using std::swap nicht weglassen, obwohl es für Vektoren eigentlich definiert sein sollte.

    void A::swap(A &lhs) {
            std::cout << "my custon swap2\n";
            using std::swap;     //hier kann ich es nicht wegmachen
            swap(v, lhs.v);
        }
    

    Ok, ohne using ist sich hier ist die Funktion selbst im Weg (unqualifiziertes Lookup findet die nicht-statische Memberfunktion, mithin wird swap in this->swap transformiert, und ADL findet nie statt). Das using std::swap dient in diesem Fall nur dazu, den Funktionsnamen zu verdecken. Du könntest z.B.

    void A::swap(A &lhs) {
            std::cout << "my custon swap2\n";
            void swap();
            swap(v, lhs.v);
        }
    

    schreiben. Ist nat. Unfug.

    //(auch in den header? aufteilen?)
    namespace std {
        template <>
        void swap(A& a, A& b)
        {
            std::cout<< "swap" << std::endl; 
            swap(a, b); 
        }
    }
    

    Überflüssig, wenn du sowieso bereits ein normales swap im Namensraum der Klasse hast. Aufteilen oder nur Header bleibt dir überlassen. Wenn alles im Header steht, muss die Funktion ausserdem als inline deklariert werden.

    Benutzernamer schrieb:

    auch und müsste ich dann nicht auch das using bei anderen Funktionen verwenden? z.b. std::move, std::sort, std::cout, ...

    Weder für std::move noch für std::sort ist eine Überladung basierend auf den Argumenttypen vorgesehen - es wäre auch kaum sinnvoll. std::cout ist ein Objekt.



  • camper schrieb:

    Überflüssig, wenn du sowieso bereits ein normales swap im Namensraum der Klasse hast. Aufteilen oder nur Header bleibt dir überlassen. Wenn alles im Header steht, muss die Funktion ausserdem als inline deklariert werden.

    Ja, es ist überflüssig es war nur Zusatz drin, falls ich es mal so machen möchte.

    camper schrieb:

    Weder für std::move noch für std::sort ist eine Überladung basierend auf den Argumenttypen vorgesehen - es wäre auch kaum sinnvoll. std::cout ist ein Objekt.

    Ah, daran habe ich nicht gedacht.

    OK, als allerletztes, wenn ich nun friend swap in meiner Klasse (im header habe). Habe ich dann so richtig in der (.cpp) auf sie verwiesen?


  • Mod

    Benutzernamer schrieb:

    OK, als allerletztes, wenn ich nun friend swap in meiner Klasse (im header habe). Habe ich dann so richtig in der (.cpp) auf sie verwiesen?

    Das ist richtig so. Eine friend-Funktion, die erst durch die friend-Deklaration erstmals deklariert wird, lebt in dem Namensraum, der die jewelige Klasse umgibt, in deren Definition die friend-Deklaration auftaucht.



  • Es ist doch noch eine, ...

    Im falle B erbt von A, dann müsste doch bei dem swap noch ein using std::swap über dem swap von Basisklasse A. Wurde zwar schon definiert aber falls das nicht der Fall ist und oder die Funktion noch einmal umdefinieren will.

    friend void swap(B& x, B& y)
        {
            using std::swap;
            swap(static_cast<A&>(x), static_cast<A&>(y)); // Basisklassenteile von x und y
            //using std::swap; //oder doch nur hier
            swap(x.asdf, y.asdf);
        }
    


  • hmm, ich habe es mal in einem größeren Programm probiert. Habe eigentlich alles so gemacht wie beschrieben (friend swap und 2 moves als Member, und bei Vererbung auf ursprüngliche Klasse verwiesen). Einziger Unterschied is, dass die Klassen pointer auf Speicherbereiche haben. Es sollte doch auch gehen wenn man move auf die pointer macht, und dann den Ursprung 0 setzt.

    Die Objekte bekommen einen pointer auf einen Bereich eines int* (intpointer), kopieren diesen Bereich (länge m) und werden schließlich in Obj-vektor vect gespeichert. Vor der Änderung mit move und swap hat alles funktioniert. Danach geht es nur noch, wenn ich alle Elemente des vektors erst erstelle und dann dem vektor hinzufüge.

    //so führt es zum segmentation fault
    for(int i = 0; n < num; i++) vect[i] = Obj{intpointer + i * m, m};
    
    //so gehts jedoch
    for(int i = 0; n < num; i++){
        Obj temp = Obj{intpointer + i * m};
        vect[i] = temp;
    }
    

    Das Initialisieren klapp mit beiden Varianten (d.h. Programm läuft weiter). Später wird ein sort gemacht und dabei geht irgendetwas schief, wenn ich die erste Variante verwende.
    "Obj" erbt von Obj0 und hat nur ein paar normale Variablen extra. Obj0 erbt wiederurm von einer anderen Klasse Objbase und hat noch einen extra pointer. Objbase speichert dann die Daten (die an intpointer + i * m liegen) in einer eigenen Kopie davon. Falls ich die moves aus einer Klasse entferne gehen auch beide Varianten.

    Die Funktionen sehen in etwa so aus:

    //class Obj : public Obj0
    Obj& operator= (Obj&& oobj) {         
        if(this == &oobj) return *this;
        Obj0::operator=(std::move(oobj));  
        return *this;
    }
    
    Obj(Obj&& oobj):Obj0(std::move(oobj)){
    }
    
    friend void swap(Obj& o1, Obj&o2){     
        if(&o1 == &o2) return;
        using std::swap;
        swap(static_cast<Obj0&>(o1), static_cast<Obj0&>(o2));                          
    }  
    
    //class  Obj0 : public Objbase
    //int* otherintpointer;
    
    Obj0& operator= (Obj0&& oobj) {         
        if(this == &oobj) return *this;
        Objbase::operator=(std::move(oobj));            
        otherintpointer = std::move(oobj.otherintpointer);   
        oobj.otherintpointer = NULL;   
        return *this;
    }
    
    Obj0(Obj0&& oobj):
        Objbase(std::move(oobj)),
        otherintpointer(std::move(oobj.otherintpointer)) {    
        oobj.otherintpointer = NULL;
    }
    
    friend void swap(Obj0& o1, Obj0&o2){
        if(&o1 == &o2) return;
        using std::swap;
        swap(static_cast<Objbase&>(o1), static_cast<Objbase&>(o2));                        
        swap(o1.otherintpointer , o2.otherintpointer);       
    }
    
    //class Objbase
    //int* intpointer; int m;
    Objbase& operator= (Objbase&& oobj) {
        if(this == &oobj) return *this;
        m = std::move( oobj.m);
        intpointer = std::move( oobj.intpointer);
        oobj.intpointer == NULL; 
        return *this;
    }
    Objbase(Objbase&& oobj) {
        m = std::move( oobj.m);
        intpointer = std::move( oobj.intpointer);
        oobj.intpointer == NULL;    
    }
    
    void swap(Objbase& o1, Objbase&o2){     
         if(&o1 == &o2) return;
         using std::swap;
         swap(o1.intpointer , o2.intpointer);      
         swap(o1.m , o2.m);
    }
    

    Was ist bzw. könnte falsch sein?



  • Hi,

    move wird auf rvalue referencen ausgeführt. Also auf temporäre Objekte. Wenn st std::move(object) machst, sagst du damit auch, dass du object nicht mehr brauchst. Da musst du nix mehr auf null pointer setzen.

    In deinem Besipiel, verwendest du auch einmal den Copy Constructor und einmal den Move Constructor. Und der Copy geht offenbar (bzw. führt nicht zu einem Absturz).

    Bei move auf Klassen mit dynamischen Speicherstellen musst du aufpassen, dass du nicht nur den Zeiger umhängst und dann eventuell ins nichts zeigst. Der einfachste (und meiner Meinung nach beste) Weg ist, Smart Pointer zu verwenden. Da funktioniert move ohne Probleme. (Dafür musst du beim unique_ptr überlegen was du beim Copy Constructor machen willst).

    Hier nochmal eine ganz gute Erklärung zum Copy & Swap Ideom: https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom

    Edit: Was willst du in Zeile 23 machen?


  • Mod

    Meine Empfehlung:
    immer using std::swap + unqualifizierten Aufruf von swap.

    Bzw. eine Komponente die das zusammenfasst. Boosts swap wäre zu erwähnen.

    Edit: Was willst du in Zeile 23 machen?

    Er will wohl die Zuweisung rekursiv auf die Basisklassen anwenden. Genau wegen solcher Komplikationen lässt man spezielle Memberfunktionen i.d.R. fast ausschließlich implizit definieren.


Anmelden zum Antworten