La mia prima risposta è stata un'introduzione estremamente semplificata per spostare la semantica, e molti dettagli sono stati lasciati fuori di proposito per renderlo semplice. Tuttavia, c'è molto di più per spostare la semantica e ho pensato che fosse tempo di una seconda risposta per colmare le lacune. La prima risposta è già piuttosto vecchia e non è giusto sostituirla semplicemente con un testo completamente diverso. Penso che serva ancora bene come prima introduzione. Ma se vuoi approfondire, continua a leggere :)
Stephan T. Lavavej ha avuto il tempo di fornire un prezioso feedback. Grazie mille, Stephan!
introduzione
Spostare la semantica consente a un oggetto, a determinate condizioni, di diventare proprietario di risorse esterne di altri oggetti. Questo è importante in due modi:
Trasformare copie costose in mosse economiche. Vedi 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), spostare la semantica non offrirà alcun vantaggio rispetto alla semantica della copia. In tal 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[64]; // moving a char array means copying a char array
// ...
};
Implementazione di tipi sicuri "solo mossa"; vale a dire tipi per i quali la copia non ha senso, ma lo spostamento ha. Gli esempi includono blocchi, handle di file e puntatori intelligenti con semantica di proprietà univoca. Nota: in questa risposta viene illustrato std::auto_ptr
un modello di libreria standard C ++ 98 obsoleto, che è stato sostituito da std::unique_ptr
in C ++ 11. I programmatori di C ++ intermedi hanno probabilmente almeno una certa familiarità con std::auto_ptr
, e a causa della "semantica di movimento" che mostra, sembra un buon punto di partenza per discutere della semantica di movimento in C ++ 11. YMMV.
Che 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 in cui non si abbia familiarità auto_ptr
, il suo scopo è garantire che un oggetto allocato in modo dinamico venga sempre rilasciato, anche a fronte di 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 auto_ptr
è il suo comportamento "copiante":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Si noti come l'inizializzazione di b
con a
non 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ò sembrare confuso perché il triangolo stesso rimane sempre nello stesso posto nella memoria.
Spostare un oggetto significa trasferire la proprietà di alcune risorse che gestisce a un altro oggetto.
Il costruttore di copie 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 auto_ptr
è che ciò che sintatticamente assomiglia a una copia è in realtà una mossa. Cercare di chiamare una funzione membro su un spostato auto_ptr
invocherà un comportamento indefinito, quindi è necessario fare molta attenzione a non utilizzare un auto_ptr
dopo che è stato spostato da:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Ma auto_ptr
non è sempre pericoloso. Le funzioni di fabbrica sono un caso d'uso perfetto per auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Nota come entrambi gli esempi seguono lo stesso modello sintattico:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Eppure, uno di loro invoca un comportamento indefinito, mentre l'altro no. Quindi qual è la differenza tra le espressioni a
e make_triangle()
? Non sono entrambi dello stesso tipo? In effetti lo sono, ma hanno diverse categorie di valore .
Categorie di valore
Ovviamente, ci deve essere una profonda differenza tra l'espressione a
che indica una auto_ptr
variabile e l'espressione make_triangle()
che indica la chiamata di una funzione che restituisce un auto_ptr
valore, creando così un nuovo auto_ptr
oggetto temporaneo ogni volta che viene chiamato. a
è un esempio di un valore , mentre make_triangle()
è un esempio di un valore .
Passare da valori come a
è pericoloso, perché in seguito potremmo provare a chiamare una funzione membro a
invocando un comportamento indefinito. D'altra parte, passare da valori come make_triangle()
è perfettamente sicuro, perché dopo che il costruttore di copie ha fatto il suo lavoro, non possiamo usare nuovamente il temporaneo. Non c'è espressione che denoti detto temporaneo; se semplicemente scriviamo di make_triangle()
nuovo, otteniamo un temporaneo diverso . In effetti, il passaggio da temporaneo è già passato alla riga successiva:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Si noti che le lettere l
e r
hanno un'origine storica nel lato lato sinistro e destro di una cessione. Questo non è più vero in C ++, perché ci sono valori che non possono apparire sul lato sinistro di un compito (come array o tipi definiti dall'utente senza un operatore di compito), e ci sono valori che possono (tutti i valori di tipi di classe con un operatore di assegnazione).
Un valore di tipo classe è un'espressione la cui valutazione crea un oggetto temporaneo. In circostanze normali, nessun'altra espressione all'interno dello stesso ambito indica lo stesso oggetto temporaneo.
Riferimenti di valore
Ora capiamo che spostarsi dai valori è potenzialmente pericoloso, ma spostarsi dai valori è innocuo. Se il C ++ avesse il supporto linguistico per distinguere gli argomenti lvalue dagli argomenti rvalue, potremmo o completamente vietare il passaggio dai lvalues o almeno rendere esplicito il passaggio dai lvalues nel sito di chiamata, in modo da non spostarci più per caso.
La risposta di C ++ 11 a questo problema sono i riferimenti di valore . Un riferimento al valore è un nuovo tipo di riferimento che si lega solo ai valori e la sintassi è X&&
. Il buon vecchio riferimento X&
è ora noto come riferimento lvalue . (Nota che nonX&&
è un riferimento a un riferimento; non esiste una cosa del genere in C ++).
Se ci buttiamo const
nel mix, abbiamo già quattro diversi tipi di riferimenti. A quali tipi di espressioni X
possono legare?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
In pratica, puoi dimenticartene const X&&
. Essere limitati a leggere dai valori non è molto utile.
Un riferimento al valore X&&
è un nuovo tipo di riferimento che si lega solo ai valori.
Conversioni implicite
I riferimenti di valore sono passati attraverso diverse versioni. Dalla versione 2.1, un riferimento a rvalue X&&
si lega anche a tutte le categorie di valori di un tipo diverso Y
, a condizione che vi sia una conversione implicita da Y
a X
. In tal caso, X
viene creato un temporaneo di tipo e il riferimento al valore è associato a quel temporaneo:
void some_function(std::string&& r);
some_function("hello world");
Nell'esempio sopra, "hello world"
è un valore di tipo const char[12]
. Poiché esiste una conversione implicita da const char[12]
attraverso const char*
a std::string
, std::string
viene creato un temporaneo di tipo ed r
è associato a tale temporaneo. Questo è uno dei casi in cui la distinzione tra valori (espressioni) e temporali (oggetti) è un po 'sfocata.
Sposta i costruttori
Un utile esempio di una funzione con un X&&
parametro è il costruttore di spostamento X::X(X&& source)
. Il suo scopo è trasferire la proprietà della risorsa gestita dall'origine all'oggetto corrente.
In C ++ 11, std::auto_ptr<T>
è stato sostituito dal std::unique_ptr<T>
quale sfrutta i riferimenti di valore. Svilupperò e discuterò una versione semplificata di unique_ptr
. Innanzitutto, incapsuliamo un puntatore non elaborato e sovraccarichiamo gli operatori ->
e *
, quindi 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 assume la proprietà dell'oggetto e il distruttore lo elimina:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Ora arriva la parte interessante, il costruttore di mosse:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Questo costruttore di mosse fa esattamente quello che ha fatto il auto_ptr
costruttore di copie, ma può essere fornito solo con valori:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
La seconda riga non può essere compilata, poiché a
è un lvalue, ma il parametro unique_ptr&& source
può essere associato solo a rvalues. Questo è esattamente quello che volevamo; le mosse pericolose non dovrebbero mai essere implicite. La terza riga si compila bene, perché make_triangle()
è un valore. Il costruttore di mosse trasferirà la proprietà dal temporaneo a c
. Ancora una volta, questo è esattamente quello che volevamo.
Il costruttore di spostamento trasferisce la proprietà di una risorsa gestita nell'oggetto corrente.
Sposta operatori di assegnazione
L'ultimo pezzo mancante è l'operatore di assegnazione delle mosse. Il suo compito è liberare 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;
}
};
Notare come questa implementazione dell'operatore di assegnazione delle mosse duplica la logica sia del distruttore che del costruttore di mosse. Conosci il linguaggio copia-e-scambia? Può anche essere applicato per spostare la semantica come 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
, verrà inizializzata dal costruttore di mosse; cioè, l'argomento verrà spostato nel parametro. L'argomento deve ancora essere un valore rvalore, poiché lo stesso costruttore di spostamento ha un parametro di riferimento valore rvalore. Quando il flusso di controllo raggiunge il controvento di chiusura operator=
, source
esce dall'ambito, rilasciando automaticamente la vecchia risorsa.
L'operatore di assegnazione degli spostamenti trasferisce la proprietà di una risorsa gestita nell'oggetto corrente, rilasciando la vecchia risorsa. Il linguaggio move-and-swap semplifica l'implementazione.
Passando dai valori
A volte, vogliamo passare dai valori. Cioè, a volte vogliamo che il compilatore tratti un valore come se fosse un valore, quindi può invocare il costruttore di mosse, anche se potrebbe essere potenzialmente pericoloso. A tale scopo, C ++ 11 offre un modello di funzione di libreria standard chiamato std::move
all'interno dell'intestazione <utility>
. Questo nome è un po 'sfortunato, perché std::move
semplicemente lancia un valore in un valore; essa non si muove nulla da solo. Si limita consente movimento. Forse avrebbe dovuto essere chiamato std::cast_to_rvalue
o std::enable_move
, ma ormai siamo bloccati con il nome.
Ecco come ti muovi esplicitamente da un valore:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Si noti che dopo la terza riga, a
non possiede più un triangolo. Va bene, perché scrivendo esplicitamentestd::move(a)
, abbiamo chiarito le nostre intenzioni: "Caro costruttore, fai tutto quello che vuoi a
per inizializzare c
; non mi interessa a
più. Sentiti libero di farcela a
."
std::move(some_lvalue)
lancia un lvalue su un rvalue, consentendo così una mossa successiva.
XValues
Si noti che anche se std::move(a)
è un valore, la sua valutazione non crea un oggetto temporaneo. Questo enigma ha costretto il comitato a introdurre una terza categoria di valore. Qualcosa che può essere associato a un riferimento di valore, anche se non è un valore in senso tradizionale, è chiamato valore x (valore eXpiring). I valori tradizionali sono stati rinominati in valori (valori puri).
Sia i valori che gli x sono valori. Xvalues e lvalues sono entrambi valori (valori generalizzati). Le relazioni sono più facili da comprendere con un diagramma:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Nota che solo gli xvalori sono davvero nuovi; il resto è solo dovuto alla ridenominazione e al raggruppamento.
I valori di C ++ 98 sono noti come valori in C ++ 11. Sostituisci mentalmente tutte le occorrenze di "rvalue" nei paragrafi precedenti con "prvalue".
Uscire dalle funzioni
Finora abbiamo visto il movimento verso variabili locali e parametri di funzione. Ma muoversi è anche possibile nella direzione opposta. Se una funzione restituisce per valore, alcuni oggetti nel sito di chiamata (probabilmente una variabile locale o temporanea, ma potrebbero essere qualsiasi tipo di oggetto) vengono inizializzati con l'espressione dopo l' return
istruzione come argomento per il costruttore di spostamenti:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Forse sorprendentemente, anche gli oggetti automatici (variabili locali che non sono dichiarate come static
) possono 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 di mosse accetta il valore result
come argomento? Lo scopo di result
sta per finire e verrà distrutto durante lo svolgimento dello stack. Nessuno avrebbe potuto lamentarsi in seguito che result
era 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 consente di restituire oggetti automatici dalle funzioni senza dover scrivere std::move
. In effetti, non si dovrebbe mai usare std::move
per spostare oggetti automatici fuori dalle funzioni, in quanto ciò inibisce l '"ottimizzazione del valore restituito" (NRVO).
Non usare mai std::move
per spostare oggetti automatici fuori dalle funzioni.
Si noti che in entrambe le funzioni di fabbrica, il tipo restituito è un valore, non un riferimento al valore. I riferimenti a valori sono ancora riferimenti e, come sempre, non si dovrebbe mai restituire un riferimento a un oggetto automatico; il chiamante finirebbe con un riferimento penzolante se inducessi il compilatore ad accettare il tuo codice, in questo modo:
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 restituire mai oggetti automatici per riferimento di valore. Lo spostamento viene eseguito esclusivamente dal costruttore dello spostamento, non da std::move
e non semplicemente legando un valore a un riferimento valore.
Trasferirsi nei membri
Prima o poi, scriverai codice in questo modo:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Fondamentalmente, il compilatore si lamenterà che parameter
è un valore. Se guardi il suo tipo, vedi un riferimento a valore, ma un riferimento a valore significa semplicemente "un riferimento che è associato a un valore"; esso non significa che il riferimento stesso è un rvalue! In effetti, parameter
è solo una normale variabile con un nome. Puoi usare tutte parameter
le volte che vuoi all'interno del corpo del costruttore e indica sempre lo stesso oggetto. Spostarsi implicitamente da esso sarebbe pericoloso, quindi la lingua lo proibisce.
Un riferimento al valore nominale è un valore lvalico, 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 viene più utilizzato dopo l'inizializzazione di member
. Perché non esiste una regola speciale da inserire silenziosamente std::move
proprio come con i valori di ritorno? Probabilmente perché sarebbe troppo onere per gli implementatori del compilatore. Ad esempio, se il corpo del costruttore fosse in un'altra unità di traduzione? Al contrario, la regola del valore restituito deve semplicemente controllare le tabelle dei simboli per determinare se l'identificatore dopo la return
parola chiave indica un oggetto automatico.
Puoi anche passare il parameter
valore. Per tipi di solo spostamento come unique_ptr
, sembra che non ci sia ancora un linguaggio consolidato. Personalmente, preferisco passare per valore, in quanto causa meno disordine nell'interfaccia.
Funzioni speciali per i membri
C ++ 98 dichiara implicitamente tre funzioni speciali dei membri su richiesta, ovvero quando sono necessarie da qualche parte: il costruttore della copia, l'operatore di assegnazione della copia e il distruttore.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
I riferimenti di valore sono passati attraverso diverse versioni. Dalla versione 3.0, C ++ 11 dichiara due ulteriori funzioni membro speciali su richiesta: il costruttore di spostamenti e l'operatore di assegnazione di spostamenti. Nota che né VC10 né VC11 sono ancora conformi alla versione 3.0, quindi dovrai implementarli tu stesso.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Queste due nuove funzioni di membri speciali vengono dichiarate implicitamente solo se nessuna delle funzioni di membri speciali viene dichiarata manualmente. Inoltre, se si dichiara il proprio costruttore di spostamento o operatore di assegnazione di spostamento, né il costruttore di copia né l'operatore di assegnazione di copia verranno dichiarati implicitamente.
Cosa significano in pratica queste regole?
Se scrivi una classe senza risorse non gestite, non è necessario dichiarare tu stesso una delle cinque funzioni speciali dei membri e otterrai la semantica della copia corretta e la muoverai gratuitamente. Altrimenti, dovrai implementare tu stesso le funzioni speciali dei membri. Naturalmente, se la tua classe non beneficia della semantica di movimento, non è necessario implementare le operazioni di spostamento speciali.
Si noti che l'operatore di assegnazione copia e l'operatore di assegnazione di spostamento possono essere fusi in un unico operatore di assegnazione unificato, prendendo il suo argomento in base al 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 speciali dei membri da implementare scende da cinque a quattro. C'è un compromesso tra sicurezza delle eccezioni ed efficienza qui, ma non sono un esperto di questo problema.
Riferimenti di inoltro ( precedentemente noti come riferimenti universali )
Considera il seguente modello di funzione:
template<typename T>
void foo(T&&);
Potresti aspettarti T&&
di legare solo ai valori, perché a prima vista sembra un riferimento al valore. A quanto pare però, T&&
si lega anche ai valori:
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 valore di tipo X
, T
si deduce che lo sia X
, quindi T&&
significa X&&
. Questo è quello che chiunque si aspetterebbe. Ma se l'argomento è un valore di tipo X
, a causa di una regola speciale, T
si ritiene che sia X&
, quindi T&&
significherebbe qualcosa di simile X& &&
. Ma dal momento che C ++ non ha ancora idea di riferimenti a riferimenti, il tipo X& &&
è crollato in X&
. Questo può sembrare inizialmente confuso e inutile, ma il collasso dei riferimenti è essenziale per un perfetto inoltro (che non verrà discusso qui).
T&& non è un riferimento di valore, ma un riferimento di inoltro. Si lega anche ai valori, nel qual caso T
e T&&
sono entrambi riferimenti ai valori.
Se vuoi vincolare un modello di funzione ai valori, puoi combinare SFINAE con tratti di tipo:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Implementazione di mosse
Ora che hai compreso il collasso dei riferimenti, ecco come std::move
viene implementato:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Come puoi vedere, move
accetta qualsiasi tipo di parametro grazie al riferimento di inoltro T&&
e restituisce un riferimento di valore. La std::remove_reference<T>::type
chiamata meta-funzione è necessaria perché altrimenti, per i valori di tipo X
, sarebbe il tipo restituito X& &&
, che collasserebbe X&
. Poiché t
è sempre un lvalue (ricorda che un riferimento a un rvalue denominato è un lvalue), ma vogliamo associarlo t
a un riferimento a rvalue, dobbiamo esplicitamente t
eseguire il cast al tipo di ritorno corretto. La chiamata di una funzione che restituisce un riferimento al valore è essa stessa un valore x. Ora sai da dove provengono gli xvalues;)
La chiamata di una funzione che restituisce un riferimento di valore, ad esempio std::move
, è un valore x.
Si noti che la restituzione per riferimento al valore va bene in questo esempio, perché t
non indica un oggetto automatico, ma invece un oggetto che è stato passato dal chiamante.