La mia prima risposta era un’introduzione estremamente semplificata alla semantica dei movimenti, e molti dettagli sono stati omessi di proposito per mantenerla semplice.Tuttavia, c’è molto di più sulla semantica dei movimenti, e ho pensato che fosse il momento di una seconda risposta per riempire le lacune.La prima risposta è già abbastanza vecchia, e non mi sembrava giusto sostituirla semplicemente con un testo completamente diverso. Penso che serva ancora bene come prima introduzione. Ma se vuoi scavare più a fondo, continua a leggere 🙂
Stephan T. Lavavej ha impiegato del tempo per fornire un prezioso feedback. Grazie mille, Stephan!
- Introduzione
- Cos’è una mossa?
- Mosse pericolose e innocue
- Categorie di valore
- Riferimenti a valori r
- Conversioni implicite
- Costruttori Move
- Operatori di assegnazione move
- Moving from lvalues
- Xvalues
- Spostamento fuori dalle funzioni
- Spostamento nei membri
- Funzioni membro speciali
- Forwarding references (precedentemente noti come riferimenti universali)
- Implementazione di move
Introduzione
La semantica Move permette ad un oggetto, sotto certe condizioni, di prendere possesso delle risorse esterne di qualche altro oggetto. Questo è importante in due modi:
-
Trasformando copie costose in mosse economiche. Vedere la mia prima risposta per un esempio. Si noti che se un oggetto non gestisce almeno una risorsa esterna (direttamente o indirettamente attraverso i suoi oggetti membri), la semantica move non offrirà alcun vantaggio rispetto alla semantica copy. In questo caso, copiare un oggetto e spostare un oggetto significa esattamente la stessa cosa:
class cannot_benefit_from_move_semantics{ int a; // moving an int means copying an int float b; // moving a float means copying a float double c; // moving a double means copying a double char d; // moving a char array means copying a char array // ...};
-
Implementare tipi sicuri “move-only”; cioè, tipi per cui copiare non ha senso, ma spostare sì. Gli esempi includono serrature, maniglie di file e puntatori intelligenti con semantica di proprietà unica. Nota: Questa risposta discute
std::auto_ptr
, un template deprecato della libreria standard C++98, che è stato sostituito dastd::unique_ptr
in C++11. I programmatori C++ intermedi hanno probabilmente almeno una certa familiarità constd::auto_ptr
, e a causa della “semantica di spostamento” che mostra, sembra un buon punto di partenza per discutere la semantica di spostamento in C++11. YMMV.
Cos’è una mossa?
La libreria standard C++98 offre un puntatore intelligente con una semantica di proprietà unica chiamata std::auto_ptr<T>
. Nel caso non abbiate familiarità con auto_ptr
, il suo scopo è quello di garantire che un oggetto allocato dinamicamente sia sempre rilasciato, anche di fronte alle eccezioni:
{ std::auto_ptr<Shape> a(new Triangle); // ... // arbitrary code, could throw exceptions // ...} // <--- when a goes out of scope, the triangle is deleted automatically
La cosa insolita di auto_ptr
è il suo comportamento “copiativo”:
auto_ptr<Shape> a(new Triangle); +---------------+ | triangle data | +---------------+ ^ | | | +-----|---+ | +-|-+ |a | p | | | | | +---+ | +---------+auto_ptr<Shape> b(a); +---------------+ | triangle data | +---------------+ ^ | +----------------------+ | +---------+ +-----|---+ | +---+ | | +-|-+ |a | p | | | b | p | | | | | +---+ | | +---+ | +---------+ +---------+
Nota come l’inizializzazione di b
con a
non copia il triangolo, ma invece trasferisce la proprietà del triangolo da a
a b
. Diciamo anche “a
viene spostato in b
” o “il triangolo viene spostato da a
a b
“. Questo può suonare confuso perché il triangolo stesso rimane sempre nello stesso posto in memoria.
Spostare un oggetto significa trasferire la proprietà di qualche risorsa che gestisce ad un altro oggetto.
Il costruttore di copia di auto_ptr
probabilmente assomiglia a questo (un po’ semplificato):
auto_ptr(auto_ptr& source) // note the missing const{ p = source.p; source.p = 0; // now the source no longer owns the object}
Mosse pericolose e innocue
La cosa pericolosa di auto_ptr
è che ciò che sintatticamente sembra una copia è in realtà uno spostamento. Cercare di chiamare una funzione membro su un auto_ptr
spostato da invocherà un comportamento non definito, quindi devi stare molto attento a non usare un auto_ptr
dopo che è stato spostato da:
auto_ptr<Shape> a(new Triangle); // create triangleauto_ptr<Shape> b(a); // move a into bdouble area = a->area(); // undefined behavior
Ma auto_ptr
non è sempre pericoloso. Le funzioni di fabbrica sono un caso d’uso perfettamente corretto per auto_ptr
:
auto_ptr<Shape> make_triangle(){ return auto_ptr<Shape>(new Triangle);}auto_ptr<Shape> c(make_triangle()); // move temporary into cdouble area = make_triangle()->area(); // perfectly safe
Nota come entrambi gli esempi seguono lo stesso schema sintattico:
auto_ptr<Shape> variable(expression);double area = expression->area();
E tuttavia, uno di essi invoca un comportamento non definito, mentre l’altro no. Allora qual è la differenza tra le espressioni a
e make_triangle()
? Non sono entrambe dello stesso tipo? In effetti lo sono, ma hanno diverse categorie di valore.
Categorie di valore
Ovviamente, ci deve essere qualche profonda differenza tra l’espressione a
che denota una variabile auto_ptr
, e l’espressione make_triangle()
che denota la chiamata di una funzione che restituisce una auto_ptr
per valore, creando così un nuovo oggetto temporaneo auto_ptr
ogni volta che viene chiamata. a
è un esempio di un lvalue, mentre make_triangle()
è un esempio di un rvalue.
Passare da lvalue come a
è pericoloso, perché potremmo successivamente provare a chiamare una funzione membro tramite a
, invocando un comportamento non definito. D’altra parte, spostarsi da valori r come make_triangle()
è perfettamente sicuro, perché dopo che il costruttore di copie ha fatto il suo lavoro, non possiamo usare di nuovo il temporaneo. Non c’è un’espressione che denota detto temporaneo; se scriviamo semplicemente make_triangle()
di nuovo, otteniamo un temporaneo diverso. Infatti, il temporaneo spostato-da è già sparito nella riga successiva:
auto_ptr<Shape> c(make_triangle()); ^ the moved-from temporary dies right here
Nota che le lettere l
e r
hanno un’origine storica nel lato sinistro e destro di un’assegnazione. Questo non è più vero in C++, perché ci sono lvalori che non possono apparire sul lato sinistro di un’assegnazione (come gli array o i tipi definiti dall’utente senza un operatore di assegnazione), e ci sono rvalori che possono (tutti gli rvalori dei tipi di classe con un operatore di assegnazione).
Un rvalore di tipo classe è un’espressione la cui valutazione crea un oggetto temporaneo. In circostanze normali, nessun’altra espressione all’interno dello stesso scope denota lo stesso oggetto temporaneo.
Riferimenti a valori r
Ora abbiamo capito che passare da lvalori è potenzialmente pericoloso, ma passare da rvalori è innocuo. Se il C++ avesse un supporto linguistico per distinguere gli argomenti lvalue dagli argomenti rvalue, potremmo o proibire completamente lo spostamento da lvalue, o almeno rendere esplicito lo spostamento da lvalue al sito della chiamata, in modo da non spostarsi più per caso.
La risposta di C++11 a questo problema sono i riferimenti rvalue. Un riferimento rvalue è un nuovo tipo di riferimento che si lega solo agli rvalori, e la sintassi è X&&
. Il buon vecchio riferimento X&
è ora conosciuto come un riferimento lvalue. (Si noti che X&&
non è un riferimento a un riferimento; non esiste una cosa del genere in C++.)
Se buttiamo const
nel mix, abbiamo già quattro diversi tipi di riferimento. A quali tipi di espressioni di tipo X
possono legarsi?
lvalue const lvalue rvalue const rvalue--------------------------------------------------------- X& yesconst X& yes yes yes yesX&& yesconst X&& yes yes
In pratica, ci si può dimenticare di const X&&
. Essere limitati a leggere da rvalori non è molto utile.
Un riferimento rvalue
X&&
è un nuovo tipo di riferimento che si lega solo a rvalori.
Conversioni implicite
I riferimenti rvalue sono passati attraverso diverse versioni. Dalla versione 2.1, un riferimento rvalue X&&
si lega anche a tutte le categorie di valori di tipo diverso Y
, purché ci sia una conversione implicita da Y
a X
. In questo caso, viene creato un temporaneo di tipo X
, e il riferimento rvalue è legato a quel temporaneo:
void some_function(std::string&& r);some_function("hello world");
Nell’esempio precedente, "hello world"
è un lvalue di tipo const char
. Poiché c’è una conversione implicita da const char
attraverso const char*
a std::string
, viene creato un temporaneo di tipo std::string
, e r
è legato a quel temporaneo. Questo è uno dei casi in cui la distinzione tra rvalori (espressioni) e temporanei (oggetti) è un po’ sfocata.
Costruttori Move
Un utile esempio di funzione con un parametro X&&
è il costruttore move X::X(X&& source)
. Il suo scopo è quello di trasferire la proprietà della risorsa gestita dalla fonte all’oggetto corrente.
In C++11, std::auto_ptr<T>
è stato sostituito da std::unique_ptr<T>
che sfrutta i riferimenti rvalue. Svilupperò e discuterò una versione semplificata di unique_ptr
. Per prima cosa, incapsuliamo un puntatore grezzo e sovraccarichiamo gli operatori ->
e *
, così la nostra classe si sente come un puntatore:
template<typename T>class unique_ptr{ T* ptr;public: T* operator->() const { return ptr; } T& operator*() const { return *ptr; }
Il costruttore prende la proprietà dell’oggetto, e il distruttore lo cancella:
explicit unique_ptr(T* p = nullptr) { ptr = p; } ~unique_ptr() { delete ptr; }
Ora arriva la parte interessante, il costruttore move:
unique_ptr(unique_ptr&& source) // note the rvalue reference { ptr = source.ptr; source.ptr = nullptr; }
Questo costruttore di spostamento fa esattamente quello che faceva il costruttore di copia auto_ptr
, ma può essere fornito solo con valori r:
unique_ptr<Shape> a(new Triangle);unique_ptr<Shape> b(a); // errorunique_ptr<Shape> c(make_triangle()); // okay
La seconda linea non riesce a compilare, perché a
è un valore l, ma il parametro unique_ptr&& source
può essere legato solo a valori r. Questo è esattamente ciò che volevamo; i movimenti pericolosi non dovrebbero mai essere impliciti. La terza linea si compila bene, perché make_triangle()
è un valore r. Il costruttore di mosse trasferirà la proprietà dal temporaneo a c
. Di nuovo, questo è esattamente quello che volevamo.
Il costruttore move trasferisce la proprietà di una risorsa gestita nell’oggetto corrente.
Operatori di assegnazione move
L’ultimo pezzo mancante è l’operatore di assegnazione move. Il suo compito è quello di rilasciare la vecchia risorsa e acquisire la nuova risorsa dal suo argomento:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference { if (this != &source) // beware of self-assignment { delete ptr; // release the old resource ptr = source.ptr; // acquire the new resource source.ptr = nullptr; } return *this; }};
Nota come questa implementazione dell’operatore di assegnazione move duplica la logica sia del distruttore che del costruttore move. Avete familiarità con l’idioma copy-and-swap? Può anche essere applicato alla semantica del movimento come l’idioma move-and-swap:
unique_ptr& operator=(unique_ptr source) // note the missing reference { std::swap(ptr, source.ptr); return *this; }};
Ora che source
è una variabile di tipo unique_ptr
, sarà inizializzata dal costruttore move; cioè, l’argomento sarà spostato nel parametro. L’argomento deve ancora essere un rvalore, perché il costruttore move stesso ha un parametro di riferimento rvalue. Quando il flusso di controllo raggiunge la parentesi graffa di chiusura di operator=
, source
esce dallo scope, rilasciando automaticamente la vecchia risorsa.
L’operatore di assegnazione move trasferisce la proprietà di una risorsa gestita nell’oggetto corrente, rilasciando la vecchia risorsa. L’idioma move-and-swap semplifica l’implementazione.
Moving from lvalues
A volte, vogliamo spostare da lvalues. Cioè, a volte vogliamo che il compilatore tratti un lvalue come se fosse un rvalue, in modo da poter invocare il costruttore move, anche se potrebbe essere potenzialmente non sicuro.Per questo scopo, C++11 offre un template di funzione di libreria standard chiamato std::move
all’interno dell’intestazione <utility>
.Questo nome è un po’ sfortunato, perché std::move
semplicemente lancia un lvalue in un rvalue; non sposta nulla da solo. Si limita a permettere lo spostamento. Forse avrebbe dovuto chiamarsi std::cast_to_rvalue
o std::enable_move
, ma ormai siamo bloccati con questo nome.
Ecco come ci si sposta esplicitamente da un valore l:
unique_ptr<Shape> a(new Triangle);unique_ptr<Shape> b(a); // still an errorunique_ptr<Shape> c(std::move(a)); // okay
Nota che dopo la terza riga, a
non possiede più un triangolo. Questo va bene, perché scrivendo esplicitamente std::move(a)
, abbiamo chiarito le nostre intenzioni: “Caro costruttore, fai quello che vuoi con a
per inizializzare c
; non mi interessa più a
. Sentiti libero di fare quello che vuoi con a
.”
std::move(some_lvalue)
fa il cast di un lvalue in un rvalue, permettendo così un successivo spostamento.
Xvalues
Nota che anche se std::move(a)
è un rvalue, la sua valutazione non crea un oggetto temporaneo. Questo enigma ha costretto il comitato a introdurre una terza categoria di valori. Qualcosa che può essere legato ad un riferimento rvalue, anche se non è un rvalue nel senso tradizionale, è chiamato un xvalue (eXpiring value). Gli rvalori tradizionali sono stati rinominati in prvalori (Pure rvalues).
Sia i prvalori che gli xvalori sono rvalori. Xvalori e lvalori sono entrambi glvalori (Generalized lvalues). Le relazioni sono più facili da capire con un diagramma:
expressions / \ / \ / \ glvalues rvalues / \ / \ / \ / \ / \ / \lvalues xvalues prvalues
Nota che solo gli xvalori sono veramente nuovi; il resto è solo dovuto alla rinominazione e al raggruppamento.
Gli rvalori di C++98 sono conosciuti come prvalori in C++11. Sostituite mentalmente tutte le occorrenze di “rvalue” nei paragrafi precedenti con “prvalue”.
Spostamento fuori dalle funzioni
Finora abbiamo visto lo spostamento in variabili locali e in parametri di funzione. Ma lo spostamento è possibile anche nella direzione opposta. Se una funzione ritorna per valore, qualche oggetto nel luogo della chiamata (probabilmente una variabile locale o un temporaneo, ma potrebbe essere qualsiasi tipo di oggetto) viene inizializzato con l’espressione dopo la dichiarazione return
come argomento del costruttore del movimento:
unique_ptr<Shape> make_triangle(){ return unique_ptr<Shape>(new Triangle);} \-----------------------------/ | | temporary is moved into c | vunique_ptr<Shape> c(make_triangle());
Forse sorprendentemente, gli oggetti automatici (variabili locali che non sono dichiarate come static
) possono anche essere implicitamente spostati fuori dalle funzioni:
unique_ptr<Shape> make_square(){ unique_ptr<Shape> result(new Square); return result; // note the missing std::move}
Come mai il costruttore move accetta il valore l result
come argomento? Lo scopo di result
sta per finire, e sarà distrutto durante lo svolgimento dello stack. Nessuno potrebbe lamentarsi in seguito che result
sia cambiato in qualche modo; quando il flusso di controllo torna al chiamante, result
non esiste più! Per questo motivo, C++11 ha una regola speciale che permette di restituire oggetti automatici dalle funzioni senza dover scrivere std::move
. Infatti, non dovreste mai usare std::move
per spostare gli oggetti automatici fuori dalle funzioni, poiché questo inibisce la “named return value optimization” (NRVO).
Non usate mai
std::move
per spostare gli oggetti automatici fuori dalle funzioni.
Nota che in entrambe le funzioni factory, il tipo di ritorno è un valore, non un riferimento rvalue. I riferimenti rvalue sono ancora riferimenti, e come sempre, non dovreste mai restituire un riferimento ad un oggetto automatico; il chiamante si ritroverebbe con un riferimento penzolante se ingannaste il compilatore ad accettare il vostro codice, come questo:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!{ unique_ptr<Shape> very_bad_idea(new Square); return std::move(very_bad_idea); // WRONG!}
Non restituite mai oggetti automatici tramite un riferimento rvalue. Lo spostamento viene eseguito esclusivamente dal costruttore move, non da
std::move
, e non legando semplicemente un rvalue a un riferimento rvalue.
Spostamento nei membri
Prima o poi, scriverete codice come questo:
class Foo{ unique_ptr<Shape> member;public: Foo(unique_ptr<Shape>&& parameter) : member(parameter) // error {}};
Fondamentalmente, il compilatore si lamenterà che parameter
è un lvalue. Se guardate il suo tipo, vedete un riferimento rvalue, ma un riferimento rvalue significa semplicemente “un riferimento che è legato a un rvalue”; non significa che il riferimento stesso sia un rvalue! Infatti, parameter
è solo una normale variabile con un nome. Potete usare parameter
tutte le volte che volete all’interno del corpo del costruttore, e denota sempre lo stesso oggetto. Spostarsi implicitamente da essa sarebbe pericoloso, quindi il linguaggio lo proibisce.
Un riferimento rvalue con nome è un lvalue, proprio come qualsiasi altra variabile.
La soluzione è abilitare manualmente lo spostamento:
class Foo{ unique_ptr<Shape> member;public: Foo(unique_ptr<Shape>&& parameter) : member(std::move(parameter)) // note the std::move {}};
Si potrebbe sostenere che parameter
non è più usato dopo l’inizializzazione di member
. Perché non c’è una regola speciale per inserire silenziosamente std::move
come per i valori di ritorno? Probabilmente perché sarebbe troppo pesante per gli implementatori del compilatore. Per esempio, cosa succederebbe se il corpo del costruttore fosse in un’altra unità di traduzione? Al contrario, la regola del valore di ritorno deve semplicemente controllare le tabelle dei simboli per determinare se l’identificatore dopo la parola chiave return
denota o meno un oggetto automatico.
Si può anche passare il parameter
per valore. Per i tipi solo movimento come unique_ptr
, sembra che non ci sia ancora un idioma stabilito. Personalmente, preferisco passare per valore, poiché causa meno disordine nell’interfaccia.
Funzioni membro speciali
C++98 dichiara implicitamente tre funzioni membro speciali su richiesta, cioè quando sono necessarie da qualche parte: il costruttore di copia, l’operatore di assegnazione di copia e il distruttore.
X::X(const X&); // copy constructorX& X::operator=(const X&); // copy assignment operatorX::~X(); // destructor
I riferimenti ai valori R hanno attraversato diverse versioni. Dalla versione 3.0, C++11 dichiara due ulteriori funzioni membro speciali su richiesta: il costruttore di spostamento e l’operatore di assegnazione di spostamento. Notate che né VC10 né VC11 sono ancora conformi alla versione 3.0, quindi dovrete implementarle voi stessi.
X::X(X&&); // move constructorX& X::operator=(X&&); // move assignment operator
Queste due nuove funzioni membro speciali sono dichiarate implicitamente solo se nessuna delle funzioni membro speciali è dichiarata manualmente. Inoltre, se dichiarate il vostro costruttore di spostamento o l’operatore di assegnazione di spostamento, né il costruttore di copia né l’operatore di assegnazione di copia saranno dichiarati implicitamente.
Cosa significano queste regole in pratica?
Se scrivete una classe senza risorse non gestite, non c’è bisogno di dichiarare nessuna delle cinque funzioni membro speciali, e avrete la corretta semantica di copia e di spostamento gratuitamente. Altrimenti, dovrete implementare voi stessi le funzioni membro speciali. Naturalmente, se la vostra classe non beneficia della semantica di spostamento, non c’è bisogno di implementare le operazioni speciali di spostamento.
Nota che l’operatore di assegnazione di copia e l’operatore di assegnazione di spostamento possono essere fusi in un singolo operatore di assegnazione unificato, prendendo il suo argomento per valore:
X& X::operator=(X source) // unified assignment operator{ swap(source); // see my first answer for an explanation return *this;}
In questo modo, il numero di funzioni membro speciali da implementare scende da cinque a quattro. C’è un compromesso tra sicurezza delle eccezioni ed efficienza qui, ma non sono un esperto in materia.
Forwarding references (precedentemente noti come riferimenti universali)
Considerate il seguente template di funzione:
template<typename T>void foo(T&&);
Vi potreste aspettare che T&&
si leghi solo a rvalori, perché a prima vista, sembra un riferimento a rvalori. Come si scopre però, T&&
si lega anche a lvalori:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&unique_ptr<Shape> a(new Triangle);foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Se l’argomento è un rvalore di tipo X
, T
è dedotto essere X
, quindi T&&
significa X&&
. Ma se l’argomento è un valore l di tipo X
, a causa di una regola speciale, T
è dedotto essere X&
, quindi T&&
significa qualcosa come X& &&
. Ma poiché il C++ non ha ancora la nozione di riferimenti a riferimenti, il tipo X& &&
è collassato in X&
. Questo può sembrare confuso e inutile all’inizio, ma il collasso dei riferimenti è essenziale per un perfetto inoltro (che non sarà discusso qui).
T&& non è un riferimento rvalue, ma un riferimento di inoltro. Si lega anche a lvalori, nel qual caso
T
eT&&
sono entrambi riferimenti lvalue.
Se volete vincolare un template di funzione agli rvalori, potete combinare SFINAE con i type traits:
#include <type_traits>template<typename T>typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::typefoo(T&&);
Implementazione di move
Ora che avete capito il collasso dei riferimenti, ecco come viene implementato std::move
:
template<typename T>typename std::remove_reference<T>::type&&move(T&& t){ return static_cast<typename std::remove_reference<T>::type&&>(t);}
Come potete vedere, move
accetta qualsiasi tipo di parametro grazie al riferimento di inoltro T&&
, e restituisce un riferimento rvalue. La chiamata alla meta-funzione std::remove_reference<T>::type
è necessaria perché altrimenti, per valori di tipo X
, il tipo di ritorno sarebbe X& &&
, che collasserebbe in X&
. Poiché t
è sempre un lvalue (ricordate che un riferimento ad un rvalue nominato è un lvalue), ma noi vogliamo legare t
ad un riferimento ad un rvalue, dobbiamo esplicitamente lanciare t
al tipo di ritorno corretto.La chiamata di una funzione che ritorna un riferimento ad un rvalue è essa stessa un xvalue. Ora sapete da dove vengono gli xvalori 😉
.