Valori C ++ 11 e spostamento della confusione semantica (istruzione return)


435

Sto cercando di capire i riferimenti ai valori e spostare la semantica di C ++ 11.

Qual è la differenza tra questi esempi e quale di essi non eseguirà alcuna copia vettoriale?

Primo esempio

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Secondo esempio

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Terzo esempio

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

51
Non restituire mai variabili locali per riferimento, mai. Un riferimento di valore è ancora un riferimento.
Fredoverflow,

63
Ciò era ovviamente intenzionale al fine di comprendere le differenze semantiche tra gli esempi lol
Tarantula,

@FredOverflow Vecchia domanda, ma mi ci è voluto un secondo per capire il tuo commento. Penso che la domanda con il n. 2 fosse se fosse stata std::move()creata una "copia" persistente.
3Dave il

5
@DavidLively std::move(expression)non crea nulla, lancia semplicemente l'espressione su un valore x. Nessun oggetto viene copiato o spostato durante il processo di valutazione std::move(expression).
Fredoverflow il

Risposte:


563

Primo esempio

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Il primo esempio restituisce un temporaneo che viene catturato da rval_ref. Quel temporaneo avrà la sua vita estesa oltre la rval_refdefinizione e puoi usarlo come se lo avessi preso per valore. Questo è molto simile al seguente:

const std::vector<int>& rval_ref = return_vector();

tranne che nel mio riscrivere ovviamente non è possibile utilizzare rval_refin modo non costante.

Secondo esempio

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Nel secondo esempio è stato creato un errore di runtime. rval_refora contiene un riferimento al distrutto tmpall'interno della funzione. Con un po 'di fortuna, questo codice si bloccherebbe immediatamente.

Terzo esempio

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Il tuo terzo esempio è approssimativamente equivalente al tuo primo. L' std::moveon tmpè inutile e può effettivamente essere una pessimizzazione delle prestazioni in quanto inibirà l'ottimizzazione del valore di ritorno.

Il modo migliore per codificare ciò che stai facendo è:

La migliore pratica

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Vale a dire come faresti in C ++ 03. tmpviene implicitamente trattato come un valore nell'istruzione return. Verrà restituito tramite l'ottimizzazione del valore di ritorno (nessuna copia, nessuna mossa) o se il compilatore decide che non può eseguire RVO, quindi utilizzerà il costruttore di mosse di vettore per fare la restituzione . Solo se RVO non viene eseguito e se il tipo restituito non avesse un costruttore di spostamento, verrà utilizzato il costruttore di copie per il ritorno.


65
I compilatori eseguiranno il RVO quando restituisci un oggetto locale in base al valore, e il tipo di locale e il ritorno della funzione sono gli stessi, e nessuno dei due è qualificato in cv (non restituisce i tipi const). Evita di tornare con la condizione (:?) In quanto può inibire l'RVO. Non racchiudere il locale in un'altra funzione che restituisce un riferimento al locale. Basta return my_local;. Le dichiarazioni di ritorno multiple sono ok e non inibiranno l'RVO.
Howard Hinnant,

27
C'è un avvertimento: quando si restituisce un membro di un oggetto locale, lo spostamento deve essere esplicito.
boicottaggio il

5
@NoSenseEtAl: non è stato creato alcun temporaneo sulla riga di ritorno. movenon crea un temporaneo. Lancia un valore a un valore x, non fa copie, non crea nulla, non distrugge nulla. Quell'esempio è esattamente la stessa situazione che si ottiene se si restituisce per lvalue-reference e si rimuove movedalla linea di ritorno: in entrambi i casi si ha un riferimento penzolante a una variabile locale all'interno della funzione e che è stata distrutta.
Howard Hinnant,

15
"Le dichiarazioni di ritorno multiple sono ok e non inibiranno l'RVO": solo se restituiscono la stessa variabile.
Deduplicatore,

5
@Deduplicator: hai ragione. Non stavo parlando con la precisione che intendevo. Intendevo dire che più istruzioni return non proibiscono al compilatore di RVO (anche se rende impossibile implementarlo), e quindi l'espressione return è ancora considerata un valore.
Howard Hinnant,

42

Nessuno di essi verrà copiato, ma il secondo farà riferimento a un vettore distrutto. I riferimenti ai valori nominali non esistono quasi mai nel codice normale. Lo scrivi proprio come avresti scritto una copia in C ++ 03.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Tranne ora, il vettore viene spostato. L' utente di una classe non si occupa dei suoi riferimenti di valore nella stragrande maggioranza dei casi.


Sei davvero sicuro che il terzo esempio eseguirà la copia vettoriale?
Tarantola,

@Tarantula: sta per sballare il tuo vettore. Non importa se lo ha fatto o meno prima di rompersi.
Cucciolo

4
Non vedo alcun motivo per il furto che proponi. È perfettamente corretto associare una variabile di riferimento del valore locale a un valore. In tal caso, la durata dell'oggetto temporaneo viene estesa alla durata della variabile di riferimento del valore.
Fredoverflow,

1
Solo un punto di chiarimento, dal momento che sto imparando questo. In questo nuovo esempio, il vettore tmpnon viene spostato in rval_ref, ma scritti direttamente rval_refutilizzando RVO (cioè copiare elision). Esiste una distinzione tra std::movee copia elisione. A std::movepuò comunque comportare la copia di alcuni dati; nel caso di un vettore, un nuovo vettore viene effettivamente costruito nel costruttore della copia e i dati vengono allocati, ma la maggior parte dell'array di dati viene copiata solo copiando il puntatore (essenzialmente). L'eliminazione della copia evita il 100% di tutte le copie.
Mark Lakata,

@MarkLakata Questo è NRVO, non RVO. NRVO è facoltativo, anche in C ++ 17. Se non viene applicato, sia il valore restituito che le rval_refvariabili vengono costruiti utilizzando il costruttore di mosse di std::vector. Non esiste alcun costruttore di copie coinvolto con / senza std::move. tmpviene trattato come un rvalue in returndichiarazione in questo caso.
Daniel Langr,

16

La semplice risposta è che dovresti scrivere il codice per i riferimenti ai valori come faresti con il codice dei riferimenti regolari e dovresti trattarli allo stesso modo il 99% delle volte. Ciò include tutte le vecchie regole sulla restituzione dei riferimenti (ovvero non restituire mai un riferimento a una variabile locale).

A meno che non si stia scrivendo una classe contenitore modello che deve sfruttare std :: forward ed essere in grado di scrivere una funzione generica che accetta riferimenti lvalue o rvalue, questo è più o meno vero.

Uno dei grandi vantaggi per il costruttore di spostamenti e l'assegnazione dei movimenti è che se li definisci, il compilatore può utilizzarli nel caso in cui RVO (ottimizzazione del valore di ritorno) e NRVO (denominata ottimizzazione del valore di ritorno) non vengano richiamati. Questo è abbastanza grande per restituire oggetti costosi come contenitori e stringhe in base al valore in modo efficiente dai metodi.

Ora dove le cose si fanno interessanti con i riferimenti rvalue, è che puoi anche usarli come argomenti per le normali funzioni. Ciò consente di scrivere contenitori con sovraccarichi sia per riferimento const (const foo e altro) sia per riferimento valore (foo && altro). Anche se l'argomento è troppo ingombrante per passare con una semplice chiamata del costruttore, può ancora essere fatto:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

I contenitori STL sono stati aggiornati per avere sovraccarichi di spostamento per quasi tutto (chiave e valori hash, inserimento vettoriale, ecc.), Ed è lì che li vedrai di più.

È inoltre possibile utilizzarli per le normali funzioni e, se si fornisce solo un argomento di riferimento al valore, è possibile forzare il chiamante a creare l'oggetto e lasciare che la funzione esegua lo spostamento. Questo è più un esempio che un ottimo uso, ma nella mia libreria di rendering ho assegnato una stringa a tutte le risorse caricate, in modo che sia più facile vedere cosa rappresenta ciascun oggetto nel debugger. L'interfaccia è qualcosa del genere:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

È una forma di "astrazione che perde" ma mi permette di sfruttare il fatto che ho dovuto creare la stringa già la maggior parte delle volte, evitando di farne ancora un'altra copia. Questo non è esattamente un codice ad alte prestazioni, ma è un buon esempio delle possibilità man mano che le persone ottengono il controllo di questa funzione. Questo codice richiede effettivamente che la variabile sia temporanea per la chiamata o invocato std :: move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

o

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

o

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

ma questo non si compila!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

Non una risposta in , ma una linea guida. Il più delle volte non ha molto senso dichiarare la T&&variabile locale (come hai fatto tu std::vector<int>&& rval_ref). Dovrai comunque std::move()usare questi foo(T&&)metodi nei tipi. C'è anche il problema che è stato già menzionato che quando si tenta di restituire tale rval_reffunzione si ottiene il riferimento standard a fiasco temporaneo distrutto.

Il più delle volte andrei con il seguente schema:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Non hai alcun riferimento agli oggetti temporanei restituiti, quindi eviti l'errore (inesperto) del programmatore che desidera utilizzare un oggetto spostato.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Ovviamente ci sono casi (anche se piuttosto rari) in cui una funzione restituisce veramente un T&&che è un riferimento a un oggetto non temporaneo che puoi spostare nel tuo oggetto.

Riguardo a RVO: questi meccanismi generalmente funzionano e il compilatore può ben evitare di copiare, ma nei casi in cui il percorso di ritorno non è ovvio (eccezioni, ifcondizioni che determinano l'oggetto nominato che si restituirà, e probabilmente accoppiare altri) i rref sono i tuoi salvatori (anche se potenzialmente più costoso).


2

Nessuno di questi farà copie extra. Anche se RVO non viene utilizzato, il nuovo standard afferma che si preferisce copiare la costruzione di mosse quando si effettuano resi.

Credo che il tuo secondo esempio provochi un comportamento indefinito perché stai restituendo un riferimento a una variabile locale.


1

Come già accennato nei commenti alla prima risposta, il return std::move(...);costrutto può fare la differenza in casi diversi dalla restituzione di variabili locali. Ecco un esempio eseguibile che documenta cosa succede quando si restituisce un oggetto membro con e senza std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Presumibilmente, return std::move(some_member);ha senso solo se si desidera spostare effettivamente un determinato membro della classe, ad esempio nel caso in cui class Crappresenti oggetti adattatore di breve durata con il solo scopo di creare istanze di struct A.

Notate come struct Asempre viene copiato fuori class B, anche quando l' class Boggetto è un valore R. Questo perché il compilatore non ha modo di dire che class Bl'istanza di struct Anon verrà più utilizzata. In class C, il compilatore ha queste informazioni da std::move(), motivo per cui struct Aviene spostato , a meno che l'istanza di class Csia costante.

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.