Utilizzando std :: move () quando si restituisce un valore da una funzione per evitare di copiare

Considera un tipo T che supporta la semantica di spostamento predefinita. Considera anche la funzione seguente:

T f() { T t; return t; } T o = f(); 

Nel vecchio C ++ 03, alcuni compilatori non ottimali potevano chiamare due volte il costruttore di copie, uno per “object restituito” e uno per ” o .

In c ++ 11, poiché t dentro f() è un lvalue, quei compilatori potrebbero chiamare il costruttore di copie una volta come prima, e quindi chiamare il costruttore di move per o.

È corretto affermare che l’unico modo per evitare la prima “copia extra” è spostarsi quando si torna?

 T f() { T t; return std::move(t); } 

No. Ogni volta che una variabile locale in un’istruzione return è idonea per copia elision, si lega a un riferimento rvalue e quindi return t; è identico a return std::move(t); nel tuo esempio rispetto a quali costruttori sono idonei.

Si noti comunque che return std::move(t); impedisce al compilatore di esercitare copia elisione, mentre return t ; no, e quindi il secondo è lo stile preferito. [Grazie a @Johannes per la correzione.] Se si verifica l’elisione della copia, la questione se utilizzare o meno la costruzione del movimento diventa un punto controverso.

Vedi 12.8 (31, 32) nello standard.

Si noti inoltre che se T ha una copia accessibile, ma un costruttore di mosse cancellato, return t; non verrà compilato, perché il costruttore di mosse deve essere considerato per primo; dovresti dire qualcosa sull’effetto di return static_cast(t); per farlo funzionare:

 T f() { T t; return t; // most likely elided entirely return std::move(t); // uses T::T(T &&) if defined; error if deleted or inaccessible return static_cast(t) // uses T::T(T const &) } 

No. La migliore pratica è direttamente return t; .

Nel caso in cui la class T non abbia cancellato il costruttore di movimento e si noti che t è una variabile locale che return t è eleggibile per copia elision, sposta costruisce l’object restituito proprio come return std::move(t); lo fa. Comunque return t; è ancora idoneo a copiare / spostare elision, quindi la costruzione può essere omessa, mentre return std::move(t) costruisce sempre il valore di ritorno usando move constructor.

Nel caso in cui il costruttore di movimento in class T venga cancellato ma sia disponibile il costruttore di copie, return std::move(t); non verrà compilato, mentre return t; compila ancora usando il costruttore di copie. A differenza di @Kerrek menzionato, t non è associato a un riferimento di rvalue. Esiste una risoluzione di sovraccarico a due stadi per i valori di ritorno idonei per l’elisione di copia: provare prima a muovere, quindi copiare, ed entrambi possono essere spostati e copiati.

 class T { public: T () = default; T (T&& t) = delete; T (const T& t) = default; }; T foo() { T t; return t; // OK: copied, possibly elided return std::move(t); // error: move constructor deleted return static_cast(t); // OK: copied, never elided } 

Se l’espressione di return è lvalue e non è idonea per la copia elisione (molto probabilmente si restituisce una variabile non locale o un’espressione lvalue) e si desidera comunque evitare la copia, std::move sarà utile. Ma tieni a mente che la pratica migliore è la possibilità di fare copia elision.

 class T { public: T () = default; T (T&& t) = default; T (const T& t) = default; }; T bar(bool k) { T a, b; return k ? a : b; // lvalue expression, copied return std::move(k ? a : b); // moved if (k) return a; // moved, and possibly elided else return b; // moved, and possibly elided } 

12.8 (32) nello standard descrive il processo.

12.8 [class.copy]

32 Quando i criteri per l’elisione di un’operazione di copia sono soddisfatti o verrebbero soddisfatti salvo il fatto che l’object di origine è un parametro di funzione e l’object da copiare è designato da un lvalue, risoluzione di sovraccarico per selezionare il costruttore per la copia viene prima eseguito come se l’object fosse designato da un valore. Se la risoluzione del sovraccarico non riesce, o se il tipo del primo parametro del costruttore selezionato non è un riferimento di valore al tipo dell’object (possibilmente qualificato cv), la risoluzione di sovraccarico viene eseguita di nuovo, considerando l’object come un valore. [Nota: questa risoluzione di sovraccarico a due stadi deve essere eseguita indipendentemente dal fatto che si verifichi l’elisione della copia. Determina il costruttore da chiamare se elision non viene eseguito e il costruttore selezionato deve essere accessibile anche se la chiamata viene eliminata. -End note]

Ok, vorrei lasciare un commento su questo. Questa domanda (e la risposta) mi ha fatto credere che non è necessario specificare std::move return. Tuttavia, ho pensato a una lezione diversa mentre gestivo il mio codice.

Quindi, ho una funzione (in realtà è una specializzazione) che prende un temporaneo e lo restituisce. (Il modello di funzione generale fa altre cose, ma la specializzazione fa l’operazione di id quadro).

 template<> struct CreateLeaf< A > { typedef A Leaf_t; inline static Leaf_t make( A &&a) { return a; } }; 

Ora, questa versione chiama il costruttore di copie di A al ritorno. Se cambio l’estratto conto in

 Leaf_t make( A &&a) { return std::move(a); } 

Quindi viene chiamato il costruttore di move di A e posso fare alcune ottimizzazioni lì.

Potrebbe non corrispondere al 100% alla tua domanda. Ma è falso pensare che return std::move(..) non sia mai necessario. Ero solito pensarlo. Non più 😉