È corretto che std::move(x)
è solo un cast da valutare - più precisamente da un valore x , al contrario di un valore . Ed è anche vero che avere un cast chiamato a move
volte confonde le persone. Tuttavia, l'intento di questa denominazione non è confondere, ma piuttosto rendere più leggibile il codice.
La storia move
risale alla proposta di trasloco originale del 2002 . Questo documento introduce prima il riferimento al valore e quindi mostra come scrivere un metodo più efficiente 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);
}
Bisogna ricordare che a questo punto della storia, l'unica cosa che " &&
" potrebbe eventualmente significare era logica e . Nessuno conosceva i riferimenti ai valori, né le implicazioni del lancio di un valore su un valore (senza fare una copia come static_cast<T>(t)
farebbe). Quindi i lettori di questo codice penserebbero naturalmente:
So come swap
dovrebbe funzionare (copia in temporaneo e poi scambia i valori), ma qual è lo scopo di quei brutti cast ?!
Si noti inoltre che swap
è davvero solo uno stand-in per tutti i tipi di algoritmi che modificano la permutazione. Questa discussione è molto , molto più grande di swap
.
Quindi la proposta introduce lo zucchero sintattico che sostituisce quello static_cast<T&&>
con qualcosa di più leggibile che trasmette non il preciso cosa , ma piuttosto il perché :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Cioè move
è solo zucchero di sintassi static_cast<T&&>
, e ora il codice è abbastanza suggestivo sul perché ci sono quei cast: per abilitare la semantica di spostamento!
Bisogna capire che nel contesto della storia, poche persone a questo punto hanno veramente capito l'intima connessione tra valori e semantica di movimento (sebbene il documento cerchi anche di spiegarlo):
La semantica di movimento entrerà automaticamente in gioco quando vengono forniti argomenti rvalue. Questo è perfettamente sicuro perché lo spostamento di risorse da un valore non può essere notato dal resto del programma ( nessun altro ha un riferimento al valore per rilevare una differenza ).
Se al momento swap
fosse invece presentato così:
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);
}
Quindi la gente avrebbe guardato quello e detto:
Ma perché stai lanciando un valore?
Il punto principale:
Com'era, usando move
, nessuno ha mai chiesto:
Ma perché ti muovi?
Col passare degli anni e la proposta è stata perfezionata, le nozioni di lvalue e rvalue sono state affinate nelle categorie di valori che abbiamo oggi:
(immagine spudoratamente rubata da dirkgently )
E così oggi, se volessimo swap
dire esattamente cosa sta facendo, invece del perché , dovrebbe assomigliare di più a:
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);
}
E la domanda che tutti dovrebbero porre è se il codice sopra è più o meno leggibile di:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
O anche l'originale:
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);
}
In ogni caso, il programmatore del c ++ del giornalista dovrebbe sapere che sotto il cofano di move
, non sta succedendo altro che un cast. E il programmatore C ++ per principianti, almeno con move
, verrà informato che l'intenzione è quella di spostarsi da rhs, invece di copiarlo da rhs, anche se non capiscono esattamente come ciò viene realizzato.
Inoltre, se un programmatore desidera questa funzionalità con un altro nome, std::move
non possiede il monopolio su questa funzionalità e non vi è alcun linguaggio magico non portatile coinvolto nella sua implementazione. Ad esempio, se si volesse programmare set_value_category_to_xvalue
e utilizzarlo invece, è banale farlo:
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);
}
In C ++ 14 diventa ancora più conciso:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Quindi, se sei così propenso, decora il tuo static_cast<T&&>
come pensi meglio, e forse finirai per sviluppare una nuova migliore pratica (C ++ è in continua evoluzione).
Quindi cosa fa move
in termini di codice oggetto generato?
Considera questo test
:
void
test(int& i, int& j)
{
i = j;
}
Compilato con clang++ -std=c++14 test.cpp -O3 -S
, questo produce questo codice oggetto:
__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
Ora se il test viene modificato in:
void
test(int& i, int& j)
{
i = std::move(j);
}
Non vi è assolutamente alcuna modifica nel codice oggetto. Si può generalizzare questo risultato a: per oggetti banalmente mobili , std::move
non ha alcun impatto.
Ora diamo un'occhiata a questo esempio:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Questo genera:
__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
Se si esegue __ZN1XaSERKS_
attraverso c++filt
essa produce: X::operator=(X const&)
. Nessuna sorpresa qui. Ora se il test viene modificato in:
void
test(X& i, X& j)
{
i = std::move(j);
}
Quindi non c'è ancora alcun cambiamento nel codice oggetto generato. std::move
non ha fatto altro che eseguire il cast j
su un valore, quindi quel valore si X
lega all'operatore di assegnazione della copia di X
.
Ora consente di aggiungere un operatore di assegnazione di spostamenti a X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Ora il codice oggetto fa il cambiamento:
__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
Il __ZN1XaSEOS_
passaggio attraverso c++filt
rivela che X::operator=(X&&)
viene chiamato invece di X::operator=(X const&)
.
E questo è tutto quello che c'è da fare std::move
! Scompare completamente in fase di esecuzione. Il suo unico impatto è in fase di compilazione in cui potrebbe alterare il cosiddetto sovraccarico.
std::move
muove in realtà ..