Co sprawia, że przenoszenie obiektów jest szybsze niż kopiowanie?

Słyszałem, jak Scott Meyers powiedział "std::move()niczego nie rusza"... ale nie rozumiem, co to znaczy.

Tak aby sprecyzować moje pytanie rozważ co następuje:

class Box { /* things... */ };

Box box1 = some_value;
Box box2 = box1;    // value of box1 is copied to box2 ... ok

A co z:

Box box3 = std::move(box1);

Rozumiem zasady lvalue i rvalue, ale nie rozumiem tego, co dzieje się w pamięci? Czy to po prostu kopiowanie wartości w inny sposób, dzielenie adresu CZY Co? Dokładniej: co sprawia, że poruszanie się jest szybsze niż kopiowanie?

Ja tylko czuję, że zrozumienie tego wszystko wyjaśni. Z góry dzięki!

EDIT: proszę zauważyć, że nie pytam o implementację std::move() ani o jakiekolwiek elementy składniowe.

Author: jotik, 2016-04-24

3 answers

@ Gudok odpowiedź przed , wszystko jest w realizacji... Wtedy bit jest w kodzie użytkownika.

Realizacja

Załóżmy, że mówimy o konstruktorze kopiującym, aby przypisać wartość do bieżącej klasy.

Implementacja, którą dostarczysz, uwzględni dwa przypadki:

  1. parametr jest wartością l, więc nie można go dotknąć, z definicji
  2. parametr jest wartością r, więc w domyśle tymczasową nie będzie dłużej żyć poza używaniem go, więc zamiast kopiować jego zawartość, możesz ukraść jego zawartość]}

Oba są zaimplementowane przy użyciu przeciążenia:

Box::Box(const Box & other)
{
   // copy the contents of other
}

Box::Box(Box && other)
{
   // steal the contents of other
}

Implementacja dla klas świetlnych

Załóżmy, że twoja klasa zawiera dwie liczby całkowite: nie możesz ukraść tych, ponieważ są one zwykłymi wartościami surowymi. Jedyną rzeczą, która mogłaby wydawać się jak kradzież byłoby skopiowanie wartości, a następnie ustawienie oryginału na zero, lub coś takiego to... Co nie ma sensu dla prostych liczb całkowitych. Po co ta dodatkowa praca?

Więc dla klas wartości lekkich, faktycznie oferowanie dwóch konkretnych implementacji, jednej dla wartości l i jednej dla wartości r, nie ma sensu.

Oferowanie tylko implementacji wartości l będzie więcej niż wystarczające.

Implementacja dla klas cięższych

Ale w przypadku niektórych ciężkich klas (np. std:: string, std:: map, itp.), kopiowanie Oznacza potencjalnie koszt, zwykle w alokacjach. Więc, idealnie, chcesz tego uniknąć jak najwięcej. To jest miejsce, gdzie kradzież dane z tymczasowych staje się interesujące.

Załóżmy, że twoje pudełko zawiera surowy wskaźnik do HeavyResource, który jest kosztowny do skopiowania. Kod staje się:

Box::Box(const Box & other)
{
   this->p = new HeavyResource(*(other.p)) ; // costly copying
}

Box::Box(Box && other)
{
   this->p = other.p ; // trivial stealing, part 1
   other.p = nullptr ; // trivial stealing, part 2
}

To oczywiste, że jeden konstruktor (Konstruktor kopiujący, który potrzebuje alokacji) jest znacznie wolniejszy od drugiego (konstruktor move, który potrzebuje tylko przypisań surowych wskaźników).

Kiedy można bezpiecznie "kraść"?

Rzecz w tym, że przez domyślnie kompilator wywoła "szybki Kod" tylko wtedy, gdy parametr jest tymczasowy (jest trochę bardziej subtelny, ale proszę mi wybaczyć...).

Dlaczego?

Ponieważ kompilator może zagwarantować, że możesz ukraść jakiś obiekt bez żadnego problemu tylko jeśli ten obiekt jest tymczasowy (lub i tak zostanie zniszczony wkrótce potem). W przypadku innych obiektów kradzież oznacza, że nagle masz obiekt, który jest ważny, ale w nieokreślonym stanie, który może być nadal używany dalej w kod. Może prowadzić do awarii lub błędów:

Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething();         // Oops! You are using an "empty" object!
Ale czasami chcesz występu. Jak to robisz?

Kod użytkownika

Jak napisałeś:

Box box1 = some_value;
Box box2 = box1;            // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???

To, co dzieje się w przypadku box2, to to, że ponieważ box1 jest wartością l, wywoływany jest pierwszy, "powolny" Konstruktor kopiujący. Jest to normalny kod C++98.

Teraz, dla box3, dzieje się coś śmiesznego: STD:: move zwraca to samo box1, ale jako odniesienie do wartości r, zamiast wartości l. Tak więc linia:

Box box3 = ...

... nie wywoĹ ' a copy-constructor na box1.

Wywoła zamiast tego konstruktor kradzieży (oficjalnie znany jako konstruktor ruchu) na box1.

I ponieważ implementacja konstruktora move dla Box ' a "kradnie" zawartość box1, na końcu wyrażenia, box1 jest w poprawnym, ale nieokreślonym stanie( zwykle będzie pusty), a box3 zawiera (poprzednią) zawartość box1.

A co z poprawnym, ale nieokreślonym stanem zajęcia z przeprowadzki?

Oczywiście, zapisanie STD:: move on a l-value oznacza, że złożysz obietnicę, że nie użyjesz tej wartości l ponownie. Albo zrobisz to bardzo, bardzo ostrożnie.

Cytowanie projektu standardu C++17 (C++11 było: 17.6.5.15):

20.5.5.15 przeniesiony - ze stanu typów bibliotek [lib .typy.movedfrom]

Obiekty typów zdefiniowanych w bibliotece standardowej C++ mogą być przenoszone z (15.8). Operacje przenoszenia mogą być jawnie określone lub niejawnie wygenerowane. Chyba że inaczej określone, takie obiekty przeniesione z powinny być umieszczone w stanie poprawnym, ale nieokreślonym.

Chodziło o typy w bibliotece standardowej, ale jest to coś, czego powinieneś przestrzegać dla własnego kodu.

Oznacza to, że wartość przeniesiona może teraz zawierać dowolną wartość, od pustej, zera lub jakiejś wartości losowej. Np. z tego, co wiesz, twój ciąg "Hello" stałby się pustym ciągiem "", lub stałby się "Hell", a nawet "Goodbye", jeśli wykonawca uważa, że jest to właściwe rozwiązanie. Nadal musi być poprawnym ciągiem znaków, z poszanowaniem wszystkich jego niezmienników.

Tak więc, w końcu, o ile wykonawca (typu) wyraźnie nie zobowiązał się do określonego zachowania po ruchu, powinieneś zachowywać się tak, jakbyś nie wiedział nic o wartości wyprowadzonej (tego typu).

Podsumowanie

Jak wspomniano powyżej, STD:: move nie robi NIC . Mówi tylko kompilatorowi: "widzisz tę wartość l? proszę uznać to za wartość r, tylko dla drugi".

Więc w:

Box box3 = std::move(box1); // ???

... kod użytkownika (np. std::move) mówi kompilatorowi, że parametr może być traktowany jako wartość r dla tego wyrażenia i w ten sposób zostanie wywołany konstruktor move.

Dla autora kodu (i recenzenta kodu) kod w rzeczywistości mówi, że można ukraść zawartość box1, aby przenieść ją do box3. Autor kodu będzie musiał upewnić się, że box1 nie jest już używany (lub używany bardzo ostrożnie). To ich odpowiedzialność.

Ale w końcu, to implementacja konstruktora move zrobi różnicę, głównie w wydajności: jeśli konstruktor move faktycznie wykrada zawartość wartości r, wtedy zobaczysz różnicę. Jeśli robi coś innego, to autor skłamał na ten temat, ale to jest inny problem...

 34
Author: paercebal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-08 09:52:41

Chodzi o implementację. Rozważmy prostą klasę string:

class my_string {
  char* ptr;
  size_t capacity;
  size_t length;
};

Semantyka copy wymaga od nas wykonania pełnej kopii ciągu znaków, w tym alokacji innej tablicy w pamięci dynamicznej i kopiowania tam zawartości *ptr, co jest kosztowne.

Semantyka move wymaga jedynie przeniesienia samej wartości wskaźnika do nowego obiektu bez powielania zawartości łańcucha znaków.

Jeśli, oczywiście, klasa nie używa pamięci dynamicznej lub zasobów systemowych, wtedy nie ma różnicy między przenoszeniem a kopiowaniem pod względem wydajności.

 14
Author: gudok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-24 19:50:57

Funkcja std::move()powinna być rozumiana jako castdo odpowiedniego typu rvalue, który umożliwia przenoszenie obiektu zamiast kopiowania.


To nie robi różnicy:

std::cout << std::move(std::string("Hello, world!")) << std::endl;

Tutaj łańcuch był już wartością R, więc std::move() niczego nie zmienił.


Może umożliwiać przenoszenie, ale nadal może skutkować kopiowaniem:

auto a = 42;
auto b = std::move(a);

Nie ma bardziej efektywnego sposobu tworzenia liczby całkowitej, która po prostu kopiuje to.


Gdzie spowoduje , gdy argument

    Jest to znak lvalue, lub lvalue reference,
  1. ma konstruktor ruchu lub przypisanie ruchu operator i
  2. jest (w sposób dorozumiany lub jawny) źródłem konstrukcji lub przypisania.

Nawet w tym przypadku, to nie move(), które faktycznie przenosi dane, to konstrukcja lub przypisanie. std:move() to po prostu obsada, która pozwala na to, nawet jeśli masz lvalue na początek. A ruch może się zdarzyć bez std::move, jeśli zaczniesz od wartości R. Myślę, że to jest sens wypowiedzi Meyersa.

 1
Author: Toby Speight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-30 09:47:29