In che modo std :: move () trasferisce i valori in RValues?


104

Mi sono appena ritrovato a non comprendere appieno la logica di std::move().

All'inizio l'ho cercato su Google ma sembra che ci siano solo documenti su come usarlo std::move(), non su come funziona la sua struttura.

Voglio dire, so qual è la funzione del membro del modello, ma quando std::move()esamino la definizione in VS2010, è ancora fonte di confusione.

la definizione di std :: move () va di seguito.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

La prima cosa strana per me è il parametro, (_Ty && _Arg), perché quando chiamo la funzione come vedi sotto,

// main()
Object obj1;
Object obj2 = std::move(obj1);

fondamentalmente è uguale a

// std::move()
_Ty&& _Arg = Obj1;

Ma come già sai, non puoi collegare direttamente un LValue a un riferimento RValue, il che mi fa pensare che dovrebbe essere così.

_Ty&& _Arg = (Object&&)obj1;

Tuttavia, questo è assurdo perché std :: move () deve funzionare per tutti i valori.

Quindi immagino che per capire appieno come funziona, dovrei dare un'occhiata anche a queste strutture.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

Purtroppo è ancora così confuso e non lo capisco.

So che è tutto a causa della mia mancanza di competenze sintattiche di base su C ++. Mi piacerebbe sapere come funzionano a fondo e qualsiasi documento che posso ottenere su Internet sarà più che benvenuto. (Se puoi spiegarlo, anche quello sarà fantastico).


31
@NicolBolas Al contrario, la domanda stessa mostra che OP si sta svendendo in quella dichiarazione. È proprio la comprensione di OP delle abilità di sintassi di base che consente loro di porre la domanda.
Kyle Strand

Comunque non sono d'accordo: puoi imparare velocemente o avere già una buona conoscenza dell'informatica o della programmazione nel suo insieme, anche in C ++, e ancora lottare con la sintassi. È meglio passare il tempo ad apprendere la meccanica, gli algoritmi, ecc. Ma fare affidamento sui riferimenti per rispolverare la sintassi secondo necessità, piuttosto che essere tecnicamente articolati ma avere una comprensione superficiale di cose come copia / sposta / avanti. Come dovresti sapere "quando e come usare std :: move quando vuoi spostare costruire qualcosa" prima di sapere cosa fa la mossa?
John P

Penso di essere venuto qui per capire come movefunziona piuttosto che come è implementato. Trovo questa spiegazione davvero utile: pagefault.blog/2018/03/01/… .
Anton Daneyko,

Risposte:


171

Iniziamo con la funzione di spostamento (che ho ripulito un po '):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Cominciamo con la parte più semplice, ovvero quando la funzione viene chiamata con rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

e il nostro movemodello viene istanziato come segue:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Poiché si remove_referenceconverte T&in To T&&in Te Objectnon è un riferimento, la nostra funzione finale è:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Ora, potresti chiederti: abbiamo anche bisogno del cast? La risposta è: sì, lo facciamo. Il motivo è semplice; il riferimento denominato rvalue viene trattato come lvalue (e la conversione implicita da lvalue a rvalue è vietata dallo standard).


Ecco cosa succede quando chiamiamo movecon lvalue:

Object a; // a is lvalue
Object b = std::move(a);

e moveistanziazione corrispondente :

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Di nuovo, remove_referenceconverte Object&in Objecte otteniamo:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Ora arriviamo alla parte difficile: cosa significa Object& &&anche e come può legarsi a lvalue?

Per consentire un inoltro perfetto, lo standard C ++ 11 fornisce regole speciali per la compressione dei riferimenti, che sono le seguenti:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Come puoi vedere, in queste regole in Object& &&realtà significa Object&, che è un semplice riferimento a lvalue che consente l'associazione di lvalues.

La funzione finale è quindi:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

che non è diverso dall'istanza precedente con rvalue: entrambi lanciano il suo argomento su riferimento rvalue e quindi lo restituiscono. La differenza è che la prima istanza può essere utilizzata solo con rvalues, mentre la seconda funziona con lvalues.


Per spiegare perché abbiamo bisogno di remove_referenceun po 'di più, proviamo questa funzione

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

e istanziarlo con lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Applicando le regole di compressione di riferimento sopra menzionate, puoi vedere che otteniamo una funzione che è inutilizzabile come move(per dirla semplicemente, la chiami con lvalue, ottieni indietro lvalue). Se non altro, questa funzione è la funzione di identità.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}

3
Bella risposta. Anche se capisco che per un valore è una buona idea valutare Tcome Object&, non sapevo che questo fosse davvero fatto. Mi sarei aspettato Tdi valutare anche Objectin questo caso, poiché pensavo che questo fosse il motivo per l'introduzione di riferimenti a wrapper e std::ref, o no.
Christian Rau

2
C'è una differenza tra template <typename T> void f(T arg)(che è ciò che è contenuto nell'articolo di Wikipedia) e template <typename T> void f(T& arg). Il primo si risolve in valore (e se vuoi passare un riferimento, devi racchiuderlo std::ref), mentre il secondo si risolve sempre in riferimento. Purtroppo, le regole per la deduzione dell'argomento del modello sono piuttosto complesse, quindi non posso fornire un ragionamento preciso perché T&&risulti a Object& &&(ma accade davvero).
Vitus

1
Ma c'è qualche motivo per cui questo approccio diretto non funzionerebbe? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo

1
Questo spiega perché remove_reference è necessario, ma ancora non riesco a capire perché la funzione deve accettare T && (invece di T &). Se capisco correttamente questa spiegazione, non dovrebbe importare se ottieni un T && o T & perché in entrambi i casi, remove_reference lo trasmetterà a T, quindi aggiungerai di nuovo &&. Allora perché non dire che accetti un T & (che semanticamente è ciò che stai accettando), invece di T && e affidarti a un inoltro perfetto per consentire al chiamante di passare un T &?
mgiuca

3
@mgiuca: Se vuoi std::movetrasmettere solo lvalues ​​a rvalues, allora sì, T&andrebbe bene. Questo trucco viene fatto principalmente per la flessibilità: puoi chiamare std::movesu tutto (rvalues ​​inclusi) e ottenere rvalue indietro.
Vitus

4

_Ty è un parametro del modello e in questa situazione

Object obj1;
Object obj2 = std::move(obj1);

_Ty è di tipo "Object &"

ecco perché il riferimento _Remove_ è necessario.

Sarebbe più simile

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Se non rimuovessimo il riferimento, sarebbe come stavamo facendo

Object&& obj2 = (ObjectRef&&)obj1_ref;

Ma ObjectRef && si riduce a Object &, che non è stato possibile associare a obj2.

Il motivo per cui si riduce in questo modo è supportare l'inoltro perfetto. Vedi questo documento .


Questo non spiega tutto sul motivo per cui _Remove_reference_è necessario. Ad esempio, se hai Object&come typedef e prendi un riferimento ad esso, ottieni comunque Object&. Perché non funziona con &&? V'è una risposta a questo, e ha a che fare con l'inoltro perfetta.
Nicol Bolas

2
Vero. E la risposta è molto interessante. A & && è ridotto ad A &, quindi se provassimo a usare (ObjectRef &&) obj1_ref, in questo caso avremmo invece Object &.
Vaughn Cato
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.