C++ std::move Frage

T_55

Lieutenant
Registriert
Feb. 2013
Beiträge
643
Hallo,

ich hab bisher std::move noch nicht genutzt daher kurze Verständnisfrage. In meinem Code unten bekommt theoretisch vec2 die Daten von vec1. Anhand der size sieht man, dass es funktioniert. Ich traue allerdings dem Braten jetzt noch nicht so ganz, bringt in diesem Fall std::move wirklich die Performance bzw ist das jetzt wirklich ganz ohne Kopieren? Ist es richtig, dass der lvalue der zur verschiebenden Daten dann im RAM an Ort und Stelle bleibt und nur der Pointer "beklaut" wird? Welchen Pointer/Speicherort bekommt dann eigentlich vec1 nachdem er beklaut wurde?

C++:
std::vector<int> vec1(10000,1);
std::vector<int> vec2;

vec2 = std::move(vec1);
 
Du musst dir vorstellen, dass std::vector intern ein dynamisches Array verwaltet, welches normalerweise vollständig kopiert werden müsste. Durch std::move kann nun aber das interne Array von vec1 einfach "geklaut" werden, indem lediglich der Zeiger auf dieses Array kopiert wird. Das ist natürlich deutlich preiswerter.

Aus diesem Grund lohnt sich ein std::move z.B. nicht bei primitiven Datentypen und Zusammensetzungen aus selbigen, weil diese so oder so kopiert werden müssen.
 
  • Gefällt mir
Reaktionen: T_55
Die reference find ich nicht immer ganz so selbsterklärend aber das Prinzip ist klar, habe ein paar Tests gemacht, Zeit gemessen und der Unterschied ist beeindruckend.

Eine Sache ist mir noch aufgefallen, wenn ich mir vor und nach std::move die Speicheradressen beider vectoren anzeigen lasse, dann bleiben sie komischerweise gleich. Müsste nicht die Speicheradresse von vec2 die von vec1 bekommen? Die Daten bleiben doch im RAM an der selben Stelle (was ja der Performancevorteil sein soll).
Was wird denn dann eigentlich genau "geklaut", wenn es nicht die Speicheradresse ist? Zudem müsste vec1 ja eine neue Speicheradresse bekommen hat aber auch noch die gleiche nach dem move.
So richtig verstanden hab ich es aus genannten Gründen noch nicht auch wenn es prima funktioniert.

C++:
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> vec1(10000,1);
    std::vector<int> vec2;

    std::cout << "vec1: " << &vec1 << std::endl;
    std::cout << "vec2: " << &vec2 << std::endl;

    vec2 = std::move(vec1);

    std::cout << "vec1: " << &vec1 << std::endl;
    std::cout << "vec2: " << &vec2 << std::endl;

    return 0;
}
 
Zuletzt bearbeitet:
T_55 schrieb:
Eine Sache ist mir noch aufgefallen, wenn ich mir vor und nach std::move die Speicheradressen beider vectoren anzeigen lasse, dann bleiben sie komischerweise gleich. Müsste nicht die Speicheradresse von vec2 die von vec1 bekommen?
Mögliche Antwort:
ph4nt0m schrieb:
Aus diesem Grund lohnt sich ein std::move z.B. nicht bei primitiven Datentypen und Zusammensetzungen aus selbigen, weil diese so oder so kopiert werden müssen.
 
@T_55
Ja, die Adresse der eigentlichen Objekte bleiben gleich. Bei std::move werden die Adressen der Member der Objekte dem Ziel Objekt zugewiesen und beim Quellobjekt zurückgesetzt. Der Vektor vec2 wird somit zu vec1 wenn man so will.

Wen du(zum verdeutlichen) auch vec2 initialisierst und dir die Adresse des ersten Elements der Vektoren anschaust wird das deutlicher.
Nach dem std::move hat das erste Element von vec2 die gleiche Adresse wie vorher erste Element von vec1.
Die Kapazität von vec1 besitzt danach den Wert 0.

Code:
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> vec1(10000, 1);
    std::vector<int> vec2(10000, 1);

    std::cout << "vec1: " << &vec1 << std::endl;
    std::cout << "vec2: " << &vec2 << std::endl;

    std::cout << "vec1_front: " << &vec1.front() << std::endl;
    std::cout << "vec2_front: " << &vec2.front() << std::endl;

    vec2 = std::move(vec1);

    std::cout << "===== After Move =====\n";

    std::cout << "vec1: " << &vec1 << std::endl;
    std::cout << "vec2: " << &vec2 << std::endl;

    std::cout << "vec1_capacity: " << vec1.capacity() << std::endl;
    std::cout << "vec2_front: " << &vec2.front() << std::endl;
    
    return 0;
}
 
  • Gefällt mir
Reaktionen: T_55
T_55 schrieb:
So richtig verstanden hab ich es aus genannten Gründen noch nicht auch wenn es prima funktioniert.
Meine Güte, dann schau Dir doch die Implementierung von std::move einfach an. Ist doch alles frei zugänglich.
 
std::move implementiert doch selbst überhaupt nichts.
Das ist ein "einfacher" static_cast. Effektiv wandelt die Funktion den Typen in eine rvalue-Referenz um. Salopp gesagt: es entfernt den Namen.
Dadurch ermöglicht es nur die Optimierungen wie im std::vector-Fall (steht auch so in der verlinkten Dokumentation).
Dann kann nämlich überhaupt erst der Move-Konstruktor oder Move-Zuweisungs-Operator aufgerufen werden.
Die Optimierung muss dann darin implementiert sein.
Hätte std::vector keine Move-Konstrutktoren/Zuweisungsoperatoren, würde überhaupt nichts optimiert werden, denn dann würden der normale Copy- oder Zuweisungsoperator aufgerufen werden.

Und zur Frage, was danach in vec2 steht: Da darf prinzipiell alles mögliche drin stehen. Du darfst dich auf nichts verlassen. Auch nicht darauf, dass überhaupt irgendwas optimiert wurde.
Das einzige, was garantiert sein muss: vec2 muss noch ganz normal destruiert werden können.
 
  • Gefällt mir
Reaktionen: T_55
Mal so als kleines Beispiel, wie eine Implementierung aussehen kann:

Code:
template<typename T>
class Array {

public:

  Array()
  : m_size(0),
    m_data(nullptr) { }

  explicit Array(size_t size) 
  : m_size(size),
    m_data(new T[size]) { }

  ~Array() {
    delete[] m_data;
  }

  Array(Array&& other)
  : m_size(other.m_size),
    m_data(other.m_data) {
    other.m_size = 0;
    other.m_data = nullptr;
  }

  Array& operator = (Array&& other) {
    delete[] m_data;
    m_size = other.m_size;
    m_data = other.m_data;
    other.m_size = 0;
    other.m_data = nullptr;
    return *this;
  }

  size_t size() const {
    return m_size;
  }

  const T& operator [] (size_t idx) const { return m_data[idx]; }
        T& operator [] (size_t idx)       { return m_data[idx]; }

private:
  
  size_t m_size;
  T*     m_data;

};

Wenn man das ganze jetzt benutzen möchte:
Code:
Array<int> a(100);
Array<int> b;
/* ... */
b = std::move(a); /* ruft Array::operator = (Array&&) auf */

Anstatt die Daten zwischen zwei Arrays hin und her zu kopieren, werden also einfach nur Pointer und Größe der eigentlichen Daten von a nach b kopiert. a wird gleichzeitig in den Ursprungszustand zurückversetzt, damit im Destruktor nicht mehrfach delete[] mit demselben Pointer ausgeführt wird.

Move-Semantics sind nicht nur eine Performance-Optimierung, sondern auch teilweise essentiell, weil nicht alle Klassen notwendigerweise Copy-Construction und Copy-Assignment implementieren, wohl aber Move-Construction und Move-Assignment implementieren können.
 
  • Gefällt mir
Reaktionen: T_55
Zurück
Oben