2013-09-05 15 views
9

La mia comprensione dell'ottimizzazione del valore di ritorno è che il compilatore passa segretamente l'indirizzo dell'oggetto in cui verrà memorizzato il valore di ritorno e apporta le modifiche a quell'oggetto anziché una variabile locale.In che modo il chiamante di una funzione sa se è stata utilizzata l'ottimizzazione del valore restituito?

Ad esempio, il codice

std::string s = f(); 

std::string f() 
{ 
    std::string x = "hi"; 
    return x; 
} 

diventa simile a

std::string s; 
f(s); 

void f(std::string& x) 
{ 
    x = "hi"; 
} 

Quando si utilizza RVO. Ciò significa che l'interfaccia della funzione è cambiata, in quanto vi è un parametro nascosto extra.

Consideriamo ora il caso seguente ho rubato da Wikipedia

std::string f(bool cond) 
{ 
    std::string first("first"); 
    std::string second("second"); 
    // the function may return one of two named objects 
    // depending on its argument. RVO might not be applied 
    return cond ? first : second; 
} 

Supponiamo che un compilatore applicherà RVO al primo caso, ma non per questo secondo caso. Ma l'interfaccia della funzione non cambia in base all'applicazione del RVO? Se il corpo della funzione f non è visibile al compilatore, come fa il compilatore a sapere se è stato applicato RVO e se il chiamante deve passare il parametro dell'indirizzo nascosto?

+3

Ogni compilatore può scegliere di farlo in qualsiasi modo desideri, e se è un problema per loro nel caso che hai descritto, probabilmente sceglieranno di usare sempre RVO. Quando sei interessato a come i diversi compilatori lo fanno sotto i cappucci, ti suggerisco di leggere il codice assemblatore generato. Usa gcc explorer per un facile accesso all'assembly generato da clang/gcc. – PlasmaHH

+0

@PlasmaHH Ma il RVO non è sempre possibile. –

+1

Ciò non significa che la convenzione di chiamata cambia quando è o non viene utilizzata. Guarda ad alcuni assemblatori come lo fanno. – PlasmaHH

risposta

7

Non c'è alcun cambiamento nell'interfaccia. In tutti i casi, i risultati della funzione devono apparire nell'ambito del chiamante; in genere, il compilatore utilizza un puntatore nascosto. L'unica differenza è che quando viene utilizzato RVO, come nel primo caso, il compilatore "unirà" x e questo valore restituito, costruendo x all'indirizzo indicato dal puntatore; quando non viene utilizzato, il compilatore genererà una chiamata al costruttore di copie nell'istruzione return , per copiare qualsiasi cosa in questo valore di ritorno.

Potrei aggiungere che il tuo secondo esempio è non molto vicino a quello che succede . Presso il sito di chiamata, si ottiene quasi sempre qualcosa di simile:

<raw memory for string> s; 
f(&s); 

E la funzione chiamata sarà o costruire una variabile locale o temporanea direttamente all'indirizzo nel quale è stata approvata, o copiare costruire un certo valore Othe a questo indirizzo. In modo che nel vostro ultimo esempio, l'istruzione return sarebbe più o meno la equivalente di:

if (cont) { 
    std::string::string(s, first); 
} else { 
    std::string::string(s, second); 
} 

(. Mostrando la this puntatore implicita passato al costruttore di copia ) Nel primo caso, se si applica RVO , il codice speciale sarebbe nel costruttore di x:

std::string::string(s, "hi"); 

e poi sostituendo x con *s ovunque nella funzione (e non fare nulla al ritorno).

+0

Quindi aggiunge un parametro nascosto e rende nulla la funzione? (chiaramente sotto il cofano) – xanatos

+2

@xanatos: non ci sono funzioni "void" a livello di assemblatore, ci sono convenzioni per cui vengono posti i valori di ritorno dei luoghi e dove il chiamante li aspetta, "void" è solo il caso in cui il numero di valori di ritorno è 0. – PlasmaHH

+1

@PlasmaHH E non ci sono parametri a livello di assemblatore ... Solo cose che qualcuno ha spinto in pila o inserito in un registro. Possiamo giocare a questo gioco tutto il giorno. Diciamo che non spinge un valore di ritorno in pila o lo mise in un registro, quindi è simile a una funzione di vuoto. – xanatos

2

Consente di giocare con NRVO, RVO e copia elision!

Qui è un tipo:

#include <iostream> 
struct Verbose { 
    Verbose(Verbose const&){ std::cout << "copy ctor\n"; } 
    Verbose(Verbose &&){ std::cout << "move ctor\n"; } 
    Verbose& operator=(Verbose const&){ std::cout << "copy asgn\n"; } 
    Verbose& operator=(Verbose &&){ std::cout << "move asgn\n"; } 
}; 

che è abbastanza prolisso.

Ecco una funzione:

Verbose simple() { return {}; } 

che è piuttosto semplice, e usa la costruzione diretta del suo valore di ritorno. Se Verbose mancasse un costruttore di copia o spostamento, la funzione sopra avrebbe funzionato!

Ecco una funzione che utilizza RVO:

Verbose simple_RVO() { return Verbose(); } 

qui l'anonimo Verbose() oggetto temporaneo è stato detto di copiare se stesso per il valore di ritorno. RVO significa che il compilatore può saltare quella copia e costruire direttamente Verbose() nel valore restituito, se e solo se esiste un costruttore di copia o spostamento. Il costruttore di copia o sposta non viene chiamato, ma piuttosto eliso.

Ecco una funzione che utilizza NRVO:

Verbose simple_NRVO() { 
    Verbose retval; 
    return retval; 
} 

Per NRVO a verificarsi, ogni percorso deve restituire lo stesso oggetto esatto, e non si può essere subdolo su di esso (se lanci il valore di ritorno a un riferimento, quindi restituire tale riferimento, che bloccherà NRVO). In questo caso, ciò che il compilatore fa è costruire l'oggetto denominato retval direttamente nel percorso del valore di ritorno. Simile a RVO, un costruttore di copia o spostamento deve esistere, ma non viene chiamato.

Ecco una funzione che non riesce a utilizzare NRVO:

Verbose simple_no_NRVO(bool b) { 
    Verbose retval1; 
    Verbose retval2; 
    if (b) 
    return retval1; 
    else 
    return retval2; 
} 

in quanto vi sono due possibili oggetti con nome che potrebbe tornare, non può costruire sia di loro nella posizione valore di ritorno, quindi deve fare una copia effettiva. In C++ 11, l'oggetto restituito sarà implicitamente move d anziché copiato, poiché è una variabile locale che viene restituita da una funzione in una semplice dichiarazione di ritorno. Quindi c'è almeno questo.

Infine, v'è la copia elisione all'altro capo:

Verbose v = simple(); // or simple_RVO, or simple_NRVO, or... 

Quando si chiama una funzione, vi fornirà con i suoi argomenti, e vi informerà che dove dovrebbe mettere il suo valore di ritorno. Il chiamante è responsabile della pulizia del valore di ritorno e dell'allocazione della memoria (in pila) per esso.

Questa comunicazione viene effettuata in qualche modo tramite la convenzione di chiamata, spesso implicitamente (cioè tramite il puntatore dello stack).

In molte convenzioni di chiamata, la posizione in cui è possibile memorizzare il valore di ritorno può essere utilizzata come variabile locale.

In generale, se si dispone di una variabile del modulo:

Verbose v = Verbose(); 

la copia implicita può essere eliso - Verbose() è costruito direttamente nel v, piuttosto che una temporanea fase di creazione quindi copiato v.Allo stesso modo, il valore di ritorno di simple (o simple_NRVO o qualsiasi altra cosa) può essere eliminato se il modello del tempo di esecuzione del compilatore lo supporta (e di solito lo fa).

In sostanza, il sito di chiamata può indicare a simple_* di inserire il valore restituito in un punto particolare e trattare tale punto come variabile locale v.

Si noti che NRVO e RVO e spostamento implicito sono tutti eseguiti all'interno della funzione e il chiamante non ha bisogno di sapere nulla al riguardo.

Analogamente, l'elisione sul sito di chiamata viene eseguita interamente all'esterno della funzione e, se la convenzione di chiamata lo supporta, non è necessario alcun supporto dal corpo della funzione.

Questo non deve essere vero in ogni convenzione di chiamata e modello di runtime, quindi lo standard C++ rende opzionali tali ottimizzazioni.

+0

se cambiamo l'esempio di "simple_no_NRVO" per costruire le variabili nei diversi se-else-casi, si applicherà nrvo? Verbose simple_no_NRVO (bool b) { \t \t if (b) \t \t { \t \t \t retval1 dettagliato; \t \t \t return retval1; \t \t} \t \t altro \t \t { \t \t \t retval2 dettagliato; \t \t \t ritorno retval2; \t \t} } –

Problemi correlati