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.
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
@PlasmaHH Ma il RVO non è sempre possibile. –
Ciò non significa che la convenzione di chiamata cambia quando è o non viene utilizzata. Guarda ad alcuni assemblatori come lo fanno. – PlasmaHH