2013-03-28 16 views
38

Prendiamo il seguente metodo come un esempio:utilizzo corretto dei riferimenti rvalue come parametri

void Asset::Load(const std::string& Path) 
{ 
    // complicated method.... 
} 

uso generale di questo metodo potrebbe essere il seguente:

Asset ExampleAsset; 
ExampleAsset.Load("image0.png"); 

Poiché sappiamo la maggior parte del tempo il Path è un valore temporaneo, ha senso aggiungere una versione Rvalue di questo metodo? E se è così, è una corretta implementazione;

void Asset::Load(const std::string& Path) 
{ 
    // complicated method.... 
} 
void Asset::Load(std::string&& Path) 
{ 
    Load(Path); // call the above method 
} 

Si tratta di un approccio corretto alla scrittura di versioni rvalue dei metodi?

risposta

47

Per il tuo caso particolare, il secondo sovraccarico è inutile.

Con il codice originale, che ha un solo sovraccarico per Load, questa funzione viene chiamata per lvalue e valori.

Con il nuovo codice, il primo overload viene chiamato per lvalue e il secondo viene chiamato per valori rvalue. Tuttavia, il secondo sovraccarico chiama il primo. Alla fine, l'effetto di chiamare uno o l'altro implica che verrà eseguita la stessa operazione (qualunque sia il primo sovraccarico).

Pertanto, gli effetti del codice originale e del nuovo codice sono gli stessi, ma il primo codice è più semplice.

Decidere se una funzione deve prendere un argomento per valore, il riferimento di lvalue o il riferimento di valore dipende molto da ciò che fa. È necessario fornire un sovraccarico prendendo i riferimenti di valore quando si desidera spostare l'argomento passato. Ci sono molti buoni references in movimento semantincs là fuori, quindi non lo copro qui.

Bonus:

per aiutarmi a fare il mio punto di prendere in considerazione questo semplice probe classe:

struct probe { 
    probe(const char* ) { std::cout << "ctr " << std::endl; } 
    probe(const probe&) { std::cout << "copy" << std::endl; } 
    probe(probe&&  ) { std::cout << "move" << std::endl; } 
}; 

Ora considerare questa funzione:

void f(const probe& p) { 
    probe q(p); 
    // use q; 
} 

Calling f("foo"); produce il seguente output:

ctr 
copy 

Nessuna sorpresa qui: creiamo un probe temporaneo passando il const char*"foo". Da qui la prima linea di uscita. Quindi, questo temporaneo è associato a p e una copia q di p viene creata all'interno di f. Da qui la seconda linea di uscita.

Ora, in considerazione di prendere p per valore, cioè, modificare f a:

void f(probe p) { 
    // use p; 
} 

L'uscita del f("foo"); è ora

ctr 

Alcuni saranno sorpresi che in questo caso: non c'è copia! In generale, se prendi un argomento per riferimento e lo copi all'interno della tua funzione, allora è meglio prendere l'argomento per valore. In questo caso, invece di creare una copia temporanea e copiarla, il compilatore può costruire l'argomento (p in questo caso) direttamente dall'input ("foo"). Per ulteriori informazioni, vedere Want Speed? Pass by Value. di Dave Abrahams.

Ci sono due eccezioni notevoli a questa linea guida: costruttori e operatori di assegnazione.

Considerate questa classe:

struct foo { 
    probe p; 
    foo(const probe& q) : p(q) { } 
}; 

Il costruttore prende un probe per riferimento const e quindi copiarlo p. In questo caso, seguire le linee guida sopra riportate non porta alcun miglioramento delle prestazioni e il costruttore di copie di probe verrà chiamato comunque. Tuttavia, assumere il valore q in base al valore potrebbe creare un problema di risoluzione di sovraccarico simile a quello con l'operatore di assegnazione che coprirò ora.

Supponiamo che la nostra classe probe abbia un metodo di non lancio swap. Poi l'attuazione suggerito del suo operatore di assegnazione (Pensare in C++ 03 termini per il momento) è

probe& operator =(const probe& other) { 
    probe tmp(other); 
    swap(tmp); 
    return *this; 
} 

Poi, secondo le linee guida di cui sopra, è meglio scrivere in questo modo

probe& operator =(probe tmp) { 
    swap(tmp); 
    return *this; 
} 

Ora immettere C++ 11 con riferimenti rvalue e spostare la semantica. Hai deciso di aggiungere un operatore di assegnamento mossa:

probe& operator =(probe&&); 

Ora chiamando l'operatore di assegnazione a titolo temporaneo crea un'ambiguità perché entrambi i sovraccarichi sono vitali e nessuno è preferito rispetto all'altro. Per risolvere questo problema, utilizzare l'implementazione originale dell'operatore di assegnazione (prendendo l'argomento con riferimento const).

In realtà, questo problema non è specifico per i costruttori e gli operatori di assegnazione e potrebbe accadere con qualsiasi funzione. (E 'più probabile che si verificherà con costruttori e operatori di assegnazione però.) Ad esempio, chiamando g("foo"); quando g ha i seguenti due overload solleva l'ambiguità:

void g(probe); 
void g(probe&&); 
+0

'Per risolvere questo problema, utilizzare l'implementazione originale dell'operatore di assegnazione (prendendo l'argomento con riferimento const) .' Invece basta applicare il common copy-and-swap-idiom implementando un costruttore di mosse e lasciando che il compilatore decida di copiare - o move- costruisce la variabile 'tmp' per l'operatore di assegnazione. – SebNag

7

A meno che non si stia facendo qualcosa di diverso dal chiamare la versione di riferimento di lvalue di Load, non è necessaria la seconda funzione, poiché un valore di r rvalue si legherà a un riferimento di const-value.

+0

È questo sempre il caso per i parametri di riferimento const? Che dire dei riferimenti non const? – Grapes

+0

Suppongo che rvalore non si leghi a un riferimento lvalue "non-const". Dovrebbe essere un errore? – Arun

+0

@Arun: Sì, hai ragione: un valore massimo non si associa a un riferimento non costante. Cercare di farlo genera un errore. –

1

E' generalmente una questione di se internamente si farà una copia (esplicitamente, o implicitamente) dell'oggetto in arrivo (fornire l'argomento T&&), altrimenti lo si utilizzerà (attenersi a [const] T&).

+2

... cosa? Questo non è assolutamente il significato accettato di un riferimento di valore. I riferimenti Rvalue vengono usati per indicare se il destinatario ** muoverà **/pilfer/altrimenti renderà inerte (ma valido) il referente, ** non ** copiarli. Copia semantica dovrebbe essere indicata da 'const &' o valore. –

1

Se la funzione membro Load non assegna dalla stringa in arrivo, è sufficiente fornire void Asset::Load(const std::string& Path).

Se si assegna dalla entrata path, dire a una variabile membro, poi c'è uno scenario in cui potrebbe essere leggermente più efficiente di fornire void Asset::Load(std::string&& Path) troppo, ma avresti bisogno di un'implementazione diversa che assegna ala loaded_from_path_ = std::move(Path);.

Il potenziale beneficio è al chiamante, in quanto con la versione && esse potrebbero ricevere regione priva di deposito che era stato proprietà della variabile membro, evitando una pessimistica delete[] ione di tale buffer all'interno void Asset::Load(const std::string& Path) ed eventuale riassegnazione la prossima volta che viene assegnata la stringa del chiamante (assumendo che il buffer sia abbastanza grande da adattarsi anche al suo valore successivo).

Nello scenario indicato, di solito si passa a stringhe letterali; tale chiamante non beneficia di alcun sovraccarico && in quanto non è presente l'istanza std::string di proprietà del chiamante per ricevere il buffer del membro dati esistente.

2

Poiché sappiamo che la maggior parte delle volte il percorso è un rimo temporaneo, ha senso aggiungere una versione Rvalue di questo metodo?

Probabilmente no ... A meno che non sia necessario fare qualcosa di complicato all'interno di Load() che richiede un parametro non-const. Ad esempio, potresti voler std::move(Path) in un'altra discussione. In tal caso potrebbe avere senso usare la semantica del movimento.

Si tratta di un approccio corretto alla scrittura di versioni rvalue dei metodi?

No, si dovrebbe fare il contrario:

void Asset::load(const std::string& path) 
{ 
    auto path_copy = path; 
    load(std::move(path_copy)); // call the below method 
} 
void Asset::load(std::string&& path) 
{ 
    // complicated method.... 
} 
Problemi correlati