Gdy zaczniesz częściej używać semantyki przenoszenia, zaczniesz znajdować przypadki, w których chcesz wywołać semantykę przenoszenia, ale obiekty, z którymi musisz pracować, to wartości l, a nie wartości r. Rozważmy jako przykład następującą funkcję zamiany:
#include <iostream>
#include <string>
template <typename T>
void mySwapCopy(T& a, T& b)
{
T tmp { a }; // invokes copy constructor
a = b; // invokes copy assignment
b = tmp; // invokes copy assignment
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
mySwapCopy(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}Przekazana w dwóch obiektach typu T (w tym przypadku std::string) ta funkcja zamienia ich wartości, tworząc trzy kopie. W rezultacie ten program drukuje:
x: abc y: de x: de y: abc
Jak pokazaliśmy na ostatniej lekcji, tworzenie kopii może być nieefektywne. Ta wersja swapu tworzy 3 kopie. Prowadzi to do nadmiernego tworzenia i niszczenia ciągów, co jest powolne.
Jednak wykonywanie kopii nie jest tutaj konieczne. Jedyne, co tak naprawdę staramy się zrobić, to zamienić wartości a i b, co równie dobrze można osiągnąć za pomocą 3 ruchów! Jeśli więc przejdziemy z semantyki kopiowania na semantykę przeniesienia, możemy zwiększyć wydajność naszego kodu.
Ale jak? Problem polega na tym, że parametry a i b są referencjami do l-wartości, a nie do wartości r, zatem nie mamy możliwości wywołania konstruktora przenoszenia i operatora przypisania przeniesienia zamiast konstruktora kopiującego i przypisania kopiującego. Domyślnie otrzymujemy konstruktor kopiujący i zachowania przypisania kopiowania. Co mamy zrobić?
std::move
W C++11 std::move jest standardową funkcją biblioteczną, która rzutuje (przy użyciu static_cast) swój argument na odwołanie do wartości r, dzięki czemu można wywołać semantykę przenoszenia. Zatem możemy użyć std::move, aby rzutować wartość l na typ, który będzie wolał być przenoszony niż kopiowany. std::move jest zdefiniowane w nagłówku narzędzia.
Oto ten sam program, co powyżej, ale z funkcją mySwapMove(), która używa std::move do konwersji naszych l-wartości na wartości r, abyśmy mogli wywołać semantykę przenoszenia:
#include <iostream>
#include <string>
#include <utility> // for std::move
template <typename T>
void mySwapMove(T& a, T& b)
{
T tmp { std::move(a) }; // invokes move constructor
a = std::move(b); // invokes move assignment
b = std::move(tmp); // invokes move assignment
}
int main()
{
std::string x{ "abc" };
std::string y{ "de" };
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
mySwapMove(x, y);
std::cout << "x: " << x << '\n';
std::cout << "y: " << y << '\n';
return 0;
}To wypisuje ten sam wynik, co powyżej:
x: abc y: de x: de y: abc
Ale jest to znacznie wydajniejsze. Kiedy inicjowany jest tmp, zamiast tworzyć kopię x, używamy std::move, aby przekonwertować zmienną x o wartości l na wartość r. Ponieważ parametr jest wartością r, wywoływana jest semantyka przenoszenia, a x jest przenoszone do tmp.
Po kilku kolejnych zamianach wartość zmiennej x została przeniesiona do y, a wartość y została przeniesiona do x.
Kolejny przykład
Możemy również użyć std::move podczas wypełniania elementów kontenera, takich jak std::vector, za pomocą l-wartości.
W poniższym programie najpierw dodajemy element do wektora, stosując semantykę kopiowania. Następnie dodajemy element do wektora za pomocą semantyki przenoszenia.
#include <iostream>
#include <string>
#include <utility> // for std::move
#include <vector>
int main()
{
std::vector<std::string> v;
// We use std::string because it is movable (std::string_view is not)
std::string str { "Knock" };
std::cout << "Copying str\n";
v.push_back(str); // calls l-value version of push_back, which copies str into the array element
std::cout << "str: " << str << '\n';
std::cout << "vector: " << v[0] << '\n';
std::cout << "\nMoving str\n";
v.push_back(std::move(str)); // calls r-value version of push_back, which moves str into the array element
std::cout << "str: " << str << '\n'; // The result of this is indeterminate
std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';
return 0;
}Na maszynie autora program ten wypisuje:
Copying str str: Knock vector: Knock Moving str str: vector: Knock Knock
W pierwszym przypadku przekazaliśmy push_back() wartość l, więc użył semantyki kopiowania, aby dodać element do wektora. Z tego powodu wartość w str zostaje pozostawiona sama.
W drugim przypadku przekazaliśmy push_back() wartość r (właściwie l-wartość przekonwertowaną za pomocą std::move), więc użyła ona semantyki przenoszenia, aby dodać element do wektora. Jest to bardziej wydajne, ponieważ element wektorowy może ukraść wartość ciągu, zamiast go kopiować.
Przeniesiony z obiektów będzie w prawidłowym, ale prawdopodobnie nieokreślonym stanie
Kiedy przenosimy wartość z obiektu tymczasowego, nie ma znaczenia, z jaką wartością pozostanie przeniesiony obiekt, ponieważ obiekt tymczasowy i tak zostanie natychmiast zniszczony. Ale co z obiektami lvalue, na których użyliśmy std::move()? Ponieważ dostęp do tych obiektów możemy nadal uzyskać po przesunięciu ich wartości (np. w powyższym przykładzie wypisujemy wartość str po przesunięciu), warto wiedzieć, z jaką wartością one pozostają.
Są tu dwie szkoły myślenia. Jedna ze szkół uważa, że obiekty, z których zostały przeniesione, powinny zostać zresetowane do stanu domyślnego/zero, w którym obiekt nie jest już właścicielem zasobu. Widzimy przykład tego powyżej, gdzie str został wyczyszczony do pustego ciągu.
Druga szkoła uważa, że powinniśmy robić to, co jest najwygodniejsze, a nie ograniczać się do konieczności czyszczenia przeniesionego obiektu, jeśli nie jest to wygodne.
Co więc robi biblioteka standardowa w tym przypadku? Standard C++ mówi o tym: „O ile nie określono inaczej, przeniesione obiekty [typów zdefiniowanych w standardowej bibliotece C++] powinny zostać umieszczone w prawidłowym, ale nieokreślonym stanie.”
W naszym powyższym przykładzie, gdy autor wydrukował wartość str po wywołaniu std::move , wydrukował pusty ciąg znaków. Nie jest to jednak wymagane i mogłoby wydrukować dowolny prawidłowy ciąg, w tym pusty ciąg, oryginalny ciąg lub dowolny inny prawidłowy ciąg. Dlatego też powinniśmy unikać używania wartości przeniesionego obiektu, gdyż rezultaty będą zależne od implementacji.
W niektórych przypadkach chcemy ponownie wykorzystać obiekt, którego wartość została przeniesiona (zamiast przydzielać nowy obiekt). Na przykład w powyższej implementacji mySwapMove() najpierw przenosimy zasób z a, a następnie przenosimy inny zasób do a. Jest to w porządku, ponieważ nigdy nie używamy wartości a między momentem, w którym go przesuwamy, a momentem, w którym nadajemy a nową określoną wartość.
W przypadku obiektu przesuniętego z obiektu można bezpiecznie wywołać dowolną funkcję, która nie zależy od bieżącej wartości obiektu. Oznacza to, że możemy ustawić lub zresetować wartość przenoszonego obiektu (za pomocą operator= lub dowolnej clear() lub reset() funkcji składowej). Możemy także przetestować stan przenoszonego obiektu (np. za pomocą empty() aby sprawdzić, czy obiekt ma wartość). Należy jednak unikać funkcji takich jak operator[] lub front() (która zwraca pierwszy element w kontenerze), ponieważ te funkcje zależą od tego, czy kontener posiada elementy, a przeniesiony z kontenera może zawierać elementy lub nie.
Kluczowa informacja
std::move() daje podpowiedź kompilatorowi, że programista nie potrzebuje już wartości obiektu. Używaj std::move() tylko na trwałych obiektach, których wartość chcesz przenieść i nie przyjmuj żadnych założeń co do wartości obiektu poza tym punktem. Można nadać przeniesionemu obiektowi nową wartość (np. za pomocą operator=) po przeniesieniu bieżącej wartości.
Gdzie jeszcze std::move jest przydatny?
std::move może być również przydatny podczas sortowania tablicy elementów. Wiele algorytmów sortowania (takich jak sortowanie przez wybór i sortowanie bąbelkowe) działa na zasadzie zamiany par elementów. Na poprzednich lekcjach musieliśmy uciekać się do semantyki kopiowania, aby dokonać zamiany. Teraz możemy użyć bardziej wydajnej semantyki przenoszenia.
Może to być również przydatne, jeśli chcemy przenieść zawartość zarządzaną przez jeden inteligentny wskaźnik do innego.
Powiązana treść
Istnieje przydatny wariant std::move() zwany std::move_if_noexcept() , który zwraca ruchomą wartość r, jeśli obiekt ma noexcept konstruktor przenoszenia, w przeciwnym razie zwraca wartość l, którą można skopiować. Omówiliśmy to w lekcji. 27.10 — std::move_if_noexcept.
Wnioski
std::move można użyć zawsze, gdy chcemy traktować wartość l jak wartość r w celu wywołania semantyki przenoszenia zamiast semantyki kopiowania.

