Perché `std :: move` si chiama` std :: move`?


128

La funzione C ++ 11 in std::move(x)realtà non sposta nulla. È solo un cast di valore r. Perché è stato fatto? Non è fuorviante?


A peggiorare le cose, la tre argomentazione si std::movemuove in realtà ..
Cubbi

E non dimenticare il C ++ 98/03/11 std::char_traits::move:-)
Howard Hinnant il

30
L'altro mio preferito è std::remove()che non rimuove gli elementi: devi ancora chiamare erase()per rimuovere effettivamente quegli elementi dal contenitore. Quindi movenon si muove, removenon si rimuove. Avrei scelto il nome mark_movable()per move.
Ali,

4
@Ali troverei mark_movable()confuso anche. Suggerisce che c'è un effetto collaterale duraturo dove in realtà non c'è nessuno.
finnw,

Risposte:


178

È 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 movevolte confonde le persone. Tuttavia, l'intento di questa denominazione non è confondere, ma piuttosto rendere più leggibile il codice.

La storia moverisale 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 swapdovrebbe 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 swapfosse 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:

Tassonomia

(immagine spudoratamente rubata da dirkgently )

E così oggi, se volessimo swapdire 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::movenon 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_xvaluee 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 movein 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::movenon 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++filtessa 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::movenon ha fatto altro che eseguire il cast jsu un valore, quindi quel valore si Xlega 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++filtrivela 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.


7
Ecco una fonte di punti per quel grafico: l'ho ricreato digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }per il bene pubblico :) Scaricalo qui come SVG
vedi il

7
È ancora aperto al ciclismo? Suggerirei allow_move;)
dyp

2
@dyp Il mio preferito è ancora movable.
Daniel Frey,

6
Scott Meyers ha suggerito di rinominare std::movea rvalue_cast: youtube.com/...
nairware

6
Dato che rvalue ora si riferisce sia a valori che a valori x, rvalue_castè ambiguo nel suo significato: che tipo di valore restituisce? xvalue_castsarebbe un nome coerente qui. Sfortunatamente, la maggior parte delle persone, in questo momento, non capirà nemmeno cosa sta facendo. Tra qualche anno, la mia affermazione si spera diventi falsa.
Howard Hinnant,

20

Permettetemi di lasciare qui una citazione dalla FAQ C ++ 11 scritta da B. Stroustrup, che è una risposta diretta alla domanda di OP:

move (x) significa "puoi considerare x come un valore". Forse sarebbe stato meglio se move () fosse stato chiamato rval (), ma ormai move () è stato usato per anni.

A proposito, mi sono piaciute molto le FAQ - vale la pena leggere.


2
Per plagiare il commento di @ HowardHinnant da un'altra risposta: la risposta di Stroustrup non è precisa, poiché ora ci sono due tipi di valori: valori e valori x, e std :: move è davvero un cast xvalue.
einpoklum,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.