C++
Copia Elision
Ricerca…
Scopo della copia elisione
Ci sono posti nello standard in cui un oggetto viene copiato o spostato per inizializzare un oggetto. Copia elision (a volte chiamata ottimizzazione del valore di ritorno) è un'ottimizzazione per cui, in determinate circostanze specifiche, un compilatore può evitare la copia o lo spostamento anche se lo standard dice che deve accadere.
Considera la seguente funzione:
std::string get_string()
{
return std::string("I am a string.");
}
In base al rigoroso testo dello standard, questa funzione inizializzerà una std::string
temporanea std::string
, quindi copierà / sposterà ciò nell'oggetto valore di ritorno, quindi distruggerà il temporaneo. Lo standard è molto chiaro che questo è il modo in cui il codice viene interpretato.
Copia elision è una regola che consente a un compilatore C ++ di ignorare la creazione del temporaneo e la sua successiva copia / distruzione. Cioè, il compilatore può prendere l'espressione di inizializzazione per il temporaneo e inizializzare direttamente il valore di ritorno della funzione. Ciò ovviamente risparmia le prestazioni.
Tuttavia, ha due effetti visibili sull'utente:
Il tipo deve avere il costruttore copia / sposta che sarebbe stato chiamato. Anche se il compilatore elude copia / sposta, il tipo deve essere ancora in grado di essere copiato / spostato.
Gli effetti collaterali dei costruttori copia / mossa non sono garantiti nelle circostanze in cui può verificarsi elisione. Considera quanto segue:
struct my_type
{
my_type() = default;
my_type(const my_type &) {std::cout <<"Copying\n";}
my_type(my_type &&) {std::cout <<"Moving\n";}
};
my_type func()
{
return my_type();
}
Cosa chiamerà func
? Bene, non stamperà mai "Copia", poiché il temporaneo è un valore rval e my_type
è un tipo spostabile. Quindi stamperà "Moving"?
Senza la regola di elisione della copia, sarebbe necessario stampare sempre "In movimento". Ma poiché esiste la regola elision di copia, il costruttore di move può o non può essere chiamato; dipende dall'implementazione.
Pertanto, non è possibile dipendere dal richiamo dei costruttori copia / sposta in contesti in cui è possibile elision.
Poiché elision è un'ottimizzazione, il compilatore potrebbe non supportare elision in tutti i casi. E indipendentemente dal fatto che il compilatore elimini un caso particolare o no, il tipo deve ancora supportare l'operazione che viene elisa. Quindi, se una costruzione di copia viene eliminata, il tipo deve ancora avere un costruttore di copia, anche se non verrà chiamato.
Copia elisione garantita
Normalmente, elision è un'ottimizzazione. Mentre praticamente tutti i compilatori supportano la copia di elision nel più semplice dei casi, l'elisione pone ancora un peso particolare agli utenti. Vale a dire, il tipo di copia / spostamento che sta per essere eliminato deve comunque avere l'operazione di copia / spostamento che è stata eli- minata.
Per esempio:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
Questo potrebbe essere utile nei casi in cui a_mutex
è un mutex che è detenuto privatamente da qualche sistema, tuttavia un utente esterno potrebbe desiderare di avere un blocco con scope.
Anche questo non è legale, perché std::lock_guard
non può essere copiato o spostato. Anche se praticamente ogni compilatore C ++ elide la copia / mossa, lo standard richiede comunque che il tipo abbia quell'operazione disponibile.
Fino al C ++ 17.
C ++ 17 impone l'elisione ridefinendo efficacemente il significato stesso di certe espressioni in modo che nessuna copia / spostamento avvenga. Considera il codice sopra.
Sotto la dicitura pre-C ++ 17, tale codice dice di creare un valore temporaneo e quindi di utilizzare il temporaneo per copiare / spostare il valore restituito, ma la copia temporanea può essere eliminata. Sotto la dicitura C ++ 17, ciò non crea affatto un temporaneo.
In C ++ 17, qualsiasi espressione di prvalore , se utilizzata per inizializzare un oggetto dello stesso tipo dell'espressione, non genera un valore temporaneo. L'espressione inizializza direttamente quell'oggetto. Se si restituisce un valore dello stesso tipo del valore restituito, il tipo non deve necessariamente avere un costruttore copia / sposta. E quindi, con le regole del C ++ 17, il codice sopra può funzionare.
La dicitura C ++ 17 funziona nei casi in cui il tipo del prvalue corrisponde al tipo da inizializzare. Quindi dato get_lock
sopra, questo non richiederà nemmeno una copia / mossa:
std::lock_guard the_lock = get_lock();
Poiché il risultato di get_lock
è un'espressione di prvalue utilizzata per inizializzare un oggetto dello stesso tipo, non si verificherà alcuna copia o spostamento. Quell'espressione non crea mai un temporaneo; è usato per inizializzare direttamente the_lock
. Non c'è elisione perché non vi è alcuna copia / mossa da elidere.
Il termine "copia elisione garantita" è quindi un termine improprio, ma questo è il nome della funzionalità, come viene proposto per la standardizzazione C ++ 17 . Non garantisce affatto l'elisione; elimina la copia / sposta del tutto, ridefinendo il C ++ in modo che non ci sia mai stata una copia / mossa da eliminare.
Questa funzione funziona solo nei casi che coinvolgono un'espressione di valore. In quanto tale, usa le solite regole di elision:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
Mentre questo è un caso valido per copia elision, le regole C ++ 17 non eliminano la copia / spostamento in questo caso. In quanto tale, il tipo deve avere ancora un costruttore di copia / spostamento da utilizzare per inizializzare il valore di ritorno. E poiché lock_guard
non lo fa, questo è ancora un errore di compilazione. Le implementazioni possono rifiutarsi di eludere le copie quando si passa o si restituisce un oggetto di tipo banalmente copiabile. Questo per consentire di spostare tali oggetti nei registri, che alcune ABI potrebbero imporre nelle loro convenzioni di chiamata.
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated
Valore di ritorno elisione
Se si restituisce un'espressione di valore da una funzione e l'espressione di prvalue ha lo stesso tipo del tipo di ritorno della funzione, è possibile elidere la copia dal valore provvisorio di prvalore:
std::string func()
{
return std::string("foo");
}
Praticamente tutti i compilatori elideranno la costruzione temporanea in questo caso.
Parametro elisione
Quando si passa un argomento a una funzione e l'argomento è un'espressione di valore del tipo di parametro della funzione e questo tipo non è un riferimento, è possibile elidere la costruzione del prvalue.
void func(std::string str) { ... }
func(std::string("foo"));
Questo dice di creare una string
temporanea, quindi spostarla nel parametro funzione str
. Copia elision permette a questa espressione di creare direttamente l'oggetto in str
, invece di usare una mossa temporanea +.
Questa è un'utile ottimizzazione per i casi in cui un costruttore è dichiarato explicit
. Ad esempio, potremmo aver scritto quanto sopra come func("foo")
, ma solo perché la string
ha un costruttore implicito che converte da un const char*
in una string
. Se quel costruttore fosse explicit
, saremmo costretti a usare un temporaneo per chiamare il costruttore explicit
. Copia elision ci salva dal dover fare una copia / mossa inutile.
Valore di ritorno denominato elisione
Se si restituisce un'espressione lvalue da una funzione e questo lvalue:
- rappresenta una variabile automatica locale per quella funzione, che verrà distrutta dopo il
return
- la variabile automatica non è un parametro di funzione
- e il tipo della variabile è dello stesso tipo del tipo di ritorno della funzione
Se tutti questi sono i casi, è possibile eliminare la copia / spostamento dal valore l:
std::string func()
{
std::string str("foo");
//Do stuff
return str;
}
Casi più complessi sono eleggibili per elision, ma più è complesso il caso, minore è la probabilità che il compilatore lo eleggi effettivamente:
std::string func()
{
std::string ret("foo");
if(some_condition)
{
return "bar";
}
return ret;
}
Il compilatore potrebbe ancora elidere ret
, ma le probabilità di loro facendo così scendere.
Come notato in precedenza, elision non è consentito per i parametri di valore.
std::string func(std::string str)
{
str.assign("foo");
//Do stuff
return str; //No elision possible
}
Copia l'inizializzazione elision
Se si utilizza un'espressione di valore per copiare l'inizializzazione di una variabile e tale variabile ha lo stesso tipo dell'espressione di valore, la copia può essere eliminata.
std::string str = std::string("foo");
L'inizializzazione della copia lo trasforma efficacemente in std::string str("foo");
(ci sono piccole differenze).
Questo funziona anche con i valori di ritorno:
std::string func()
{
return std::string("foo");
}
std::string str = func();
Senza copiare elision, ciò provocherebbe 2 chiamate al costruttore di spostamenti di std::string
. Copia elision consente di chiamare il costruttore di movimento 1 o zero volte e la maggior parte dei compilatori sceglierà quest'ultimo.