Prawdą jest, że std::move(x)jest to tylko rzutowanie na rwartość - a dokładniej na xwartość , w przeciwieństwie do prwartości . Prawdą jest również, że imię obsady moveczasami dezorientuje ludzi. Jednak celem tego nazewnictwa nie jest zmylenie, ale raczej uczynienie kodu bardziej czytelnym.
Historia movesięga do pierwotnej propozycji przeniesienia z 2002 roku . Ten artykuł najpierw wprowadza odniesienie do wartości r, a następnie pokazuje, jak napisać bardziej wydajne std::swap:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Należy przypomnieć, że w tym momencie historii jedyną rzeczą, która &&mogłaby oznaczać, była logika i . Nikt nie był zaznajomiony z odniesieniami do rwartości, ani z implikacjami rzutowania lwartości na rwartość (nie robiąc kopii tak static_cast<T>(t), jak by to zrobił). Czytelnicy tego kodu naturalnie pomyśleliby:
Wiem, jak swapma działać (kopiowanie na tymczasowe, a potem zamiana wartości), ale jaki jest cel tych brzydkich odlewów ?!
Zauważ również, że swapjest to tak naprawdę tylko zastępstwo dla wszelkiego rodzaju algorytmów modyfikujących permutacje. Ta dyskusja jest dużo , dużo większa niż swap.
Następnie propozycja wprowadza cukier składniowy, który zastępuje static_cast<T&&>coś bardziej czytelnym, co nie wyjaśnia dokładnie, co , ale raczej dlaczego :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
To moveznaczy jest po prostu cukierkiem składniowym static_cast<T&&>, a teraz kod sugeruje, dlaczego istnieją te rzutowania: aby włączyć semantykę przenoszenia!
Trzeba zrozumieć, że w kontekście historii niewiele osób w tym momencie naprawdę zrozumiało intymny związek między wartościami r i semantyką ruchu (choć artykuł próbuje to również wyjaśnić):
Po podaniu argumentów rvalue automatycznie włącza się semantyka ruchu. Jest to całkowicie bezpieczne, ponieważ przenoszenie zasobów z wartości r nie może zostać zauważone przez resztę programu ( nikt inny nie ma odniesienia do wartości r w celu wykrycia różnicy ).
Jeśli w tamtym czasie swapzostał przedstawiony w ten sposób:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Wtedy ludzie spojrzeliby na to i powiedzieli:
Ale dlaczego rzucasz na rvalue?
Główny punkt:
Tak jak było, używając move, nikt nigdy nie zapytał:
Ale dlaczego się przeprowadzasz?
W miarę upływu lat i udoskonalania propozycji, pojęcia lwartości i rwartości zostały dopracowane do kategorii wartości, które mamy dzisiaj:

(obraz bezwstydnie skradziony z dirkgently )
I tak dzisiaj, gdybyśmy chcieli swapprecyzyjnie powiedzieć, co robi, zamiast dlaczego , powinien wyglądać bardziej tak:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
I pytanie, które każdy powinien sobie zadać, brzmi: czy powyższy kod jest mniej lub bardziej czytelny niż:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Albo nawet oryginał:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
W każdym razie czeladnik programista C ++ powinien wiedzieć, że pod maską movenie dzieje się nic więcej niż obsada. A początkujący programista C ++, przynajmniej z move, zostanie poinformowany, że jego zamiarem jest przejście z prawej strony, a nie kopiowanie z prawej strony, nawet jeśli nie rozumieją dokładnie, jak to się robi .
Dodatkowo, jeśli programista życzy sobie tej funkcjonalności pod inną nazwą, std::movenie ma monopolu na tę funkcjonalność i nie ma nieprzenośnej magii językowej związanej z jej implementacją. Na przykład, jeśli ktoś chciałby kodować set_value_category_to_xvaluei zamiast tego używać tego, jest to trywialne:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
W C ++ 14 robi się jeszcze bardziej zwięźle:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Więc jeśli masz taką skłonność, udekoruj swój sposób, w static_cast<T&&>jaki myślisz najlepiej, a być może w końcu opracujesz nową najlepszą praktykę (C ++ stale się rozwija).
Więc co robi movew zakresie wygenerowanego kodu obiektowego?
Rozważ to test:
void
test(int& i, int& j)
{
i = j;
}
Skompilowane za pomocą clang++ -std=c++14 test.cpp -O3 -S, generuje ten kod wynikowy:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Teraz, jeśli test zostanie zmieniony na:
void
test(int& i, int& j)
{
i = std::move(j);
}
Nie ma absolutnie żadnej zmiany w kodzie wynikowym. Wynik ten można uogólnić tak, że: dla obiektów trywialnie poruszających sięstd::move nie ma żadnego wpływu.
Spójrzmy teraz na ten przykład:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
To generuje:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
W przypadku uruchomienia __ZN1XaSERKS_przez c++filtprodukuje: X::operator=(X const&). Nic dziwnego. Teraz, jeśli test zostanie zmieniony na:
void
test(X& i, X& j)
{
i = std::move(j);
}
Wtedy nadal nie ma żadnej zmiany w wygenerowanym kodzie obiektowym. std::movenie zrobił nic poza rzutowaniem jna wartość r, a następnie ta wartość r Xwiąże się z operatorem przypisania kopiowania z X.
Teraz dodajmy operator przypisania przenoszenia do X:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Teraz kod obiektowy się zmienia:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Przejście __ZN1XaSEOS_przez c++filtobjawienia, które X::operator=(X&&)są wywoływane zamiast X::operator=(X const&).
I to wszystko std::move! Znika całkowicie w czasie wykonywania. Jego jedyny wpływ ma miejsce w czasie kompilacji, gdzie może zmienić wywołanie przeciążenia.
std::move