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 move
czasami dezorientuje ludzi. Jednak celem tego nazewnictwa nie jest zmylenie, ale raczej uczynienie kodu bardziej czytelnym.
Historia move
się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 swap
ma działać (kopiowanie na tymczasowe, a potem zamiana wartości), ale jaki jest cel tych brzydkich odlewów ?!
Zauważ również, że swap
jest 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 move
znaczy 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 swap
został 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 swap
precyzyjnie 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ą move
nie 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::move
nie 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_xvalue
i 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 move
w 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++filt
produkuje: 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::move
nie zrobił nic poza rzutowaniem j
na wartość r, a następnie ta wartość r X
wiąż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++filt
objawienia, 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