Move Konstruktor Funktionsweise / Vorgehen



  • Hallo zusammen

    Ich benötige Hilfe beim Verständnis des Move-Konstruktors.
    Suche nun schon seit Stunden, finde aber nichts das mir meine Frage beantworten kann.

    Wie funktioniert der Move-Konstruktor?
    Mir ist klar, dass damit Werte von einem Objekt in ein anderes verschoben werden.
    Mit std::move wird aus dem Quellobjekt (lvalue) ein rvalue erzeugt von welchem die Werte "geklaut" werden.
    Was ich nun aber nicht vestehe ist was genau beim verschieben passiert.
    Wenn der Move-Konstruktor ausgeführt wird, werden per Elementinitialisierer die Werte vom Quellobjekt auf das neue Objekt übertragen.
    Werden nun aber im Anweisungsblock des Konstruktors die Werte des Quellobjektes nicht auf Standardwerte zurück gesetzt, bleibt dieses Objekt nach dem "Verschieben" unverändert.

    Ich verstehe daher nicht wie das genaue Vorgehen beim Verschieben lautet.
    Wenn beim erstellen des rvalues ein temporäres Objekt erzeugt wird, kommt es da nicht auf dasselbe raus wie beim kopieren, nämlich dass erst eine Kopie erzeugt werden muss von welcher die werte übernommen werden?

    Ich hoffe ihr versteht mein verständnisproblem und könnt mich erleuchten.

    Gruss
    Inmeining.



  • Inmeining schrieb:

    Wenn beim erstellen des rvalues ein temporäres Objekt erzeugt wird,[snip]

    ein rvalue muss kein temporäres objekt sein! du "markierst" mit dem std::move einfach nur die eigentumsverhältnisse am objekt, und zwar sagst du, dass die anwendung des move-constructors erlaubt ist - das heißt, dass das ursprüngliche objekt (ob temporary oder nicht) "leer" ist. bei einem temporären objekt ist das implizit erlaubt.

    (genauer gesagst machst du mit dem std::move aus einem lvalue kein rvalue sondern ein xvalue - das ist ein lvalue mit der erlaubnis, einen move-constructor zu verwenden, also das originalobjekt zu zerstören)

    wenn du es verabsäumst, das original zurückzusetzen, dann hast du plötzlich eine doppelte eigentümerschaft an ressourcen - inklusive aller probleme (z.b. doppeltes delete).



  • Vielleicht das wichtigste:

    Ein move-Konstruktor, der einfach wie ein Copy-Konstruktor arbeitet, ist ok. Und wenn du nur "einfache" Datentypen hast, wird der Move-Konstruktor auch nichts anderes tun als kopieren.

    Interessant wird es erst, wenn du zum Beispiel irgendwo große Datenmengen hast, auf die ein Pointer zeigt. Dann kannst du im move-Konstruktor einfach den Pointer des Ursprungsobjekts klauen (und musst nicht eine "große" Kopie anlegen) und den im Ursprungsobjekt z.B. auf nullptr setzen (bzw. dem Ursprungsobjekt irgendwie anders sagen, dass es nicht mehr löschen muss). Auch möglich: einfach ein neues leeres Objekt erstellen und dann ein swap mit dem move-from machen.

    Denk mal an einen vector mit 100.000 Zahlen. Der Copy-Konstruktor muss also die 100.000 Zahlen umkopieren. Wenn du die im move-from-Object aber nicht mehr brauchst, dann spricht ja nichts dagegen, einfach genau diese Zahlen im neuen Objekt zu verwenden und eben dem alten wegzunehmen (ergo einfach pointer auf den Datenbereich klauen). Also: du darfst das move-from-Objekt ändern, es darf "kaputt sein", nur muss danach noch sein Destruktor erfolgreich aufgerufen werden können.

    So, das war jetzt ganz untechnisch die Idee dahinter.

    PS: mit "klauen" meine ich kopieren und dann dem Ursprungsbesitzer dann was anderes unterschieben.



  • Alles klar, das mit dem xvalue habe ich mal soweit begriffen, aber was bedeutet "leer"?

    Mit doppelter Eigentümerschaft meist du, dass die Eigenschaften beider Objekte auf die selbe Speicheradresse zielen?



  • mit "leer" meinte ich, dass das (ursprüngliche) objekt sicher zerstört werden kann und nichts enthält (keine zeiger/handles), was mittlerweile anderen objekten gehört (eigentümerschaft).

    und mit eigentümerschaft meine ich: ein eigentümer ist zuständig dafür, eine ressource (dynamischer speicher, dateihandle, thread, etc.) freizugeben. es kann und darf nur einen eigentümer geben.

    ein copy-ctor erzeugt eine kopie - also eine zweite ressource: das ursprüngliche objekt ist weiterhin eigentümer aller seiner ressourcen, das neue objekt erhält neue ressourcen und ist eigentümer davon.

    ein move-ctor "holt" (daher "move") sich die eigentümerschaft vom ursprünglichen objekt. d.h. das ursprüngliche objekt darf nicht mehr eigentümer sein (darf die ressourcen nicht selbst freigeben). eine möglichkeit, das zu tun, ist etwa zeiger auf nullptr zu setzen (weil delete nullptr nichts bewirkt). du kannst dem ursprungsobjekt auch irgendwie anders mitteilen, dass es sich nicht um die freigabe der ressourcen kümmern muss:

    class Moving {
        ressource handle;
        bool owner = true;
    
    public:
        Moving (moving&& other) noexcept : handle{std::move(other.handle)} { other.owner = false; }
    
        ~Moving () {
          if (owner) release_and_free(handle); 
        }
    };
    

    wob schrieb:

    Vielleicht das wichtigste:

    Ein move-Konstruktor, der einfach wie ein Copy-Konstruktor arbeitet, ist ok.

    das halte ich für keinen guten ratschlag... (ich weiß, ist eh nicht so gemeint)

    ein move-konstruktor, der wie ein copy-konstruktor arbeitet ist außerdem nicht exception-safe.



  • kümmern muss kümmern darf.



  • Danke euch beiden für die Antwort.
    Also machen Move-Konstruktoren besonders bei der Verwendung von Zeigern oder Multithreading Sinn, da über sie die Eigentümerschaft einem anderen Objekt zugewiesen und dem Orignal einfach entzogen werden kann?

    In wie fern machen Move-Konstruktoren den bei der Verwendung von Basisdatentypen Sinn?

    class myclass
    {
         int ival;
         double dval;
    public:
         myclass(myclass &&old) : ival(old.ival), dval(old.dval)
         {
              old.ival = 0;
              old.dval = 0.0;
         }
    };
    

    Macht ein Verschiebe-Konstruktor mit solchen Eigenschaften überhaupt Sinn?
    Denn hier gibt es ja keinen Zeiger über welchen die Eigentümerschaft geändert werden kann?

    Danke für eure Gelduld 😃



  • richtig, in deinem beispiel macht ein move-konstruktor überhaupt keinen sinn - und es ist auch völlig unnötig, old.ival und old.dval auf null zu setzen.

    wenn man das einmal versteht, sieht man auch, dass es oft einfach überhaupt unnötig ist, sich um selbst geschriebene move, copy, default konstruktoren und destruktoren zu kümmern.

    class myclass {
       int ival;
       double dval;
       std::string s;
    
    public:
       myclass(myclass &&) = default;
       myclass(myclass const&) = default
       myclass& operator=(myclass const&) = default;
       myclass& operator=(myclass&&) = default; 
    };
    

    siehe auch die rule of three/five/zero

    rule of three (schon in C++98)
    wenn eine klasse eine der drei funktionen: (1) copy ctor (2) copy-op= oder (3) dtor definiert und nicht die default-variante des compilers verwendet, ist es ziemlich sicher nötig, auch die anderen beiden funktionen zu definieren.

    rule of five:
    zusätzlich zu copy ctor, copy-op= und dtor kommen noch move-ctor und move-op= dazu.

    rule of zero:
    für klassen, die keine ressourcen freigeben müssen (eigentümerschaft), sollte man auch keine der fünf funktionen selbst definieren. klassen, die sich um ressourcen kümmern (eigentümerschaft), sollen sich ausschließlich darum kümmern.



  • dove schrieb:

    ein move-konstruktor, der wie ein copy-konstruktor arbeitet ist außerdem nicht exception-safe.

    So generell ist das falsch. Gegenbeispiel ist eine Klasse mit nur einer int-Variablen als Member, die sonst nix tut. Aber klar, so war das nicht gemeint 😉

    Inmeining schrieb:

    In wie fern machen Move-Konstruktoren den bei der Verwendung von Basisdatentypen Sinn?

    Ist völlig sinnlos, da move in diesem Fall nix anderes tut als kopieren.
    (nen pointer ist auch ein Basisdatentyp, aber den meintest du nicht, richtig?)



  • wob schrieb:

    Inmeining schrieb:

    In wie fern machen Move-Konstruktoren den bei der Verwendung von Basisdatentypen Sinn?

    Ist völlig sinnlos, da move in diesem Fall nix anderes tut als kopieren.
    (nen pointer ist auch ein Basisdatentyp, aber den meintest du nicht, richtig?)

    ich weiß nicht, was ihr damit genau meint.

    dass eine basisklasse einen move-konstruktor zur verfügung stellt, kann schon sinnvoll sein. std::basic_ostream macht das z.b.



  • Ich hatte das so verstanden, dass mit "Basisdatentyp" sowas "POD-artiges ohne Pointer" gemeint war (das Beispiel hatte ja nur int und double).

    Glaube nicht, dass Inmeining damit ne Basisklasse gemeint hatte (was du offenbar daraus verstanden hast). Wenn doch, dass ergibt meine Antwort natürlich keinen Sinn.



  • jo, ich hatte zuerst auch überhaupt schwierigkeiten, nachzuvollziehen, auf was sich dein zitat bezogen hat, wohl weil Inmeining das noch nachträglich editiert hat...



  • Danke für eure Antworten, es ist mir soweit glaube ich nun klar.
    Ja ich meinte eine Klasse mit Basisdatentypen.
    Was mich nicht ganz klar war, war dass es erst bei Klassen Sinvoll ist, die bspw. Zeiger oder Multithreading enthalten.


Anmelden zum Antworten