Anfänger std::move



  • Hier gibt es einen brauchbaren Artikel zu dem Thema. Der beschreibt auch, was lvalues und rvalues überhaupt sind. Außerdem geht er auch auf die typischen Anfängerfehler bei den rvalue-Referenzen ein.



  • Ganz simpel zusammengefasst, machen Move-Konstruktoren und move-Assignment nur dann Sinn, wenn du in der Klasse eine oder mehrere Ressourcen nach RAII-Art hälst.
    Bisher hieß Rvalue-Kopieren von solchen Klassen immer folgendes:

    Kopierendes Objekt legt neue Ressource an, die die Ressource des Originals kopiert.
    Originalobjekt wird zerstört, mit ihm die Originalressource.

    Move-Operationen machen es wie folgt:
    Kopierendes Objekt übernimmt die Ressource des Originals. Keine tiefen Kopien nötig, kein Anlegen der neuen Ressource nötig.
    Originalobjekt wird (ohne Originalressource) zerstört.

    Stell dir eine sehr vereinfachte Version des std::vector vor. Er enhält einen Pointer auf die Daten (dynamisches Array) und ein size_t, das weiß, wieviele Daten es sind, d.h. wie groß das Array ist. Der Copy-Ctor kopiert den size_t, legt ein neues dynamisches Array an und kopiert den Inhalt des Original-Arrays ins neue Array. Das Original stirbt, zerstört alle Element und gibt das Array frei.
    Der Move-Ctor kopiert den size_t (bei einfache Datentypen gibts nichts zu moven!), kopiert danach den Pointer und setzt den Pointer im Original auf 0. Nimmt also quasi dem Original seine Ressource (das dynamische Array) einfach weg. Das Original stirbt und nimmt nur seinen size_t mit ins Grab.



  • Tachyon, der von Dir verlinkte Artikel ist leider nicht mehr aktuell. Es gab seitdem zwei wichtige Änderungen bei den Rvalue-Referenz-Regeln. Heute verweist eine frisch initialisierte non-const Rvalue-Referenz immer nur auf ein Objekt, was verändert werden darf, ohne dass es jemanden stören würde. Anfang 2009 konnte man aber noch schreiben

    int    i  = 0;
    int && rr = i;
    

    Das kompiliert jetzt aber nicht mehr. Man hat aus Sicherheitsgründen die Regeln abgeändert, weil es sonst zu versehentlichen Modifikationen von anderen Objekten hätte kommen können.

    Wer gesprochenes amerikanisches Englisch versteht, kann sich ja mal Stephan T. Lavavej's Vortrag anhören.



  • Hallo,
    ich habe mal in einer Klasse den Move-Copy-Ctor und den Move Assignment Operator implementiert. Falls etwas besser gemacht werden könnte, bitte sagen.

    #include <iostream>
    #include <vector>
    #include <string>
    
    class X
    {
    private:
    	std::vector<std::string> * m_pResource;
    public:
    	X():m_pResource(new std::vector<std::string>) {}
    	~X() { delete m_pResource; 
    			m_pResource = nullptr;}
    
    	X(X const& other); //normal copy ctor
    	X(X&& tmp); //move copy ctor
    	X& operator= (X const& rhs); //normal assignment operator
    	X& operator=(X&& tmp); //Move assignment operator
    
    };
    
    X::X(X const& other)
    {
    	std::cout << "Copy Ctor\n";
    	m_pResource = new std::vector<std::string>(*other.m_pResource); //teuer
    }
    
    X::X(X&& tmp)
    {
    	std::cout << "Move Copy Ctor\n";
    	m_pResource = nullptr;
    	std::swap(m_pResource,tmp.m_pResource);
    }
    
    X& X::operator=(X&& tmp)
    {
    	std::cout << "Move Assignmnet Operator\n";
    	std::swap(m_pResource,tmp.m_pResource);
    	return *this;
    }
    
    X& X::operator= (X const& rhs)
    {	
    	if (this != &rhs)
    	{
    		std::cout << "Assignment operator\n";
    		std::vector<std::string> * tmp = new std::vector<std::string>(*rhs.m_pResource); //teuer
    		std::swap(m_pResource,tmp);
    	}
    	return *this;
    }
    
    X foo() 
    {
    	X x;
    	return x;
    }
    
    int main()
    {
    	X x;
    	std::cout << "x = foo(); \n";
    	x = foo(); //Ohne Move Ctor und ohne Move Assignment operator wird zuerst der Copy-Ctor aufgerufen und anschließend der Assignment operator aufgerufen (ohne NRVO)
    			   //in beiden Fällen wird die Resource teuer kopiert 
    				//mit NRVO: nur der Assignment operator wird aufgerufen
    
    			  //mit Move Copy-Ctor und Move Assignmnet operator wird zuerst der MoveCopy-Ctor und anschließend der Move Assignment operator aufgerufen (ohne NRVO)
    			  //in beiden Fällen werden nur Pointer umgebogen
    			  //mit NRVO: nur der Move Assignment operator wird aufgerufen
    
    	std::cout << "X y = x \n";
    	X y =x; //x ist kein rvlaue -> copy ctor
    	std::cout << "X v; v = x;\n";
    	X v;
    	v = x; //x ist kein rvlaue  -> assignment operator
    	std::cout << "X z;z = X();\n";
    	X z;
    	z = X(); // X() ist ein Rvalue -> move assignment operator
    	std::cout << "X t = std::move(z);\n";
    	X t = std::move(z);// z wird zum Rvalue -> move copy ctor (auf z sollte nicht mehr zugegriffen werden)
    	std::cout << "X c;c = std::move(t);\n";
    	X c;
    	c = std::move(t); //t wird zum Rvalue -> move assignment operator (auf t sollte nicht mehr zugegriffen werden)
    
    	return 0;
    }
    

  • Mod

    einwurf schrieb:

    Falls etwas besser gemacht werden könnte, bitte sagen.

    Der kopierende Zuweisungsoperator führt etwas aus, was du bereits implementiert hast (und auch noch falsch, weil du den Speicher nicht freigibst). Zweckmäßigerweise sollte er eher so aussehen (Copy+Move, statt wie bisher Copy+Swap):

    X& X::operator= (X const& rhs)
    {   
        std::cout << "Assignment operator\n";
        return *this = X(rhs);
    }
    

    Außerdem prüfst du beim Kopieren nicht, ob evtl. das Argument bereits leer ist.



  • [quote="camper"]

    einwurf schrieb:

    X& X::operator= (X const& rhs)
    {   
        return *this = X(rhs);
    }
    

    Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?



  • @camper, stimmt. Eine Speicherlücke.
    Es geht aber noch einfacher (nur ein operator=), wenn ich nicht wieder etwas übersehen habe.

    class X
    {
    private:
    	std::vector<std::string> * m_pResource;
    public:
    	X():m_pResource(new std::vector<std::string>) {}
    	~X() { delete m_pResource; 
    			m_pResource = nullptr;}
    
    	X(X const& other); //normal copy ctor
    	X(X&& tmp); //move copy ctor
    	X& operator= (X rhs); //Move assignment operator and normal assignment operator
    
    };
    
    X::X(X const& other)
    {
    	std::cout << "Copy Ctor\n";
    	m_pResource = new std::vector<std::string>(*other.m_pResource); //teuer
    }
    
    X::X(X&& tmp)
    {
    	std::cout << "Move Copy Ctor\n";
    	m_pResource = nullptr;
    	std::swap(m_pResource,tmp.m_pResource);
    }
    
    X& X::operator= (X rhs)
    {  
    	std::cout << "Move and normal assignment operator\n";
        if(this != &rhs) 
    	{
    		std::swap(m_pResource, rhs.m_pResource);
        }
    	return *this;
    }
    


  • Wenn ich auch mal einen Einwurf machen dürfte: Die Dinger heißen copy constructor, copy assignment operator, move constructor und move assignment operator. Insbesondere "Move-Copy-Ctor" ergibt keinen Sinn.



  • @Bashar, danke. Ich dachte irrtümlich, weil der Copy-Ctor überladen wird heißt das nun ...

    Das if(this != &rhs) kann man sich auch sparen, da "rhs" ja immer ein anderes Objekt als "this" ist.
    Das swappen für den Fall x=x; sollte keine Probleme machen, oder?

    X& X::operator= (X rhs)
    {  
        std::cout << "Move and normal assignment operator\n";
        std::swap(m_pResource, rhs.m_pResource);
        return *this;
    }
    

  • Mod

    einwurf schrieb:

    @camper, stimmt. Eine Speicherlücke.
    Es geht aber noch einfacher (nur ein operator=), wenn ich nicht wieder etwas übersehen habe.

    ...
    

    Das ist so möglich, aber im Movefall führst du so u.U. 2 Moves aus.


  • Mod

    Fraaaggger schrieb:

    camper schrieb:

    X& X::operator= (X const& rhs)
    {   
        return *this = X(rhs);
    }
    

    Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?

    X(rhs) erzeugt ein temporäres Objekt, das dann für die Zuweisung genutzt wird. Weil es sich um ein rvalue handelt, wird die andere Zuweisungsüberladung aufgerufen, und somit das zurückgegeben, was die andere Funktion zurückgibt, also *this.



  • camper schrieb:

    Fraaaggger schrieb:

    camper schrieb:

    X& X::operator= (X const& rhs)
    {   
        return *this = X(rhs);
    }
    

    Was wird denn hier zurückgegeben? Also was ist der Rückgabewert einer zuweisung?

    X(rhs) erzeugt ein temporäres Objekt, das dann für die Zuweisung genutzt wird. Weil es sich um ein rvalue handelt, wird die andere Zuweisungsüberladung aufgerufen, und somit das zurückgegeben, was die andere Funktion zurückgibt, also *this.

    Ahh! Jetzt machts sinn, danke! 🙂


Anmelden zum Antworten