Perché dovrei usare push_back invece di emplace_back?

I vettori C ++ 11 hanno la nuova funzione emplace_back . A differenza di push_back , che si affida alle ottimizzazioni del compilatore per evitare copie, emplace_back utilizza l’inoltro perfetto per inviare gli argomenti direttamente al costruttore per creare un object sul posto. Mi sembra che emplace_back faccia tutto ciò che push_back può fare, ma alcune volte lo farà meglio (ma mai peggio).

Quale motivo devo usare push_back ?

push_back consente sempre l’utilizzo dell’inizializzazione uniforms, a cui sono molto affezionato. Per esempio:

 struct aggregate { int foo; int bar; }; std::vector v; v.push_back({ 42, 121 }); 

D’altra parte, v.emplace_back({ 42, 121 }); non funzionerà.

Ho pensato a questa domanda un po ‘negli ultimi quattro anni. Sono giunto alla conclusione che la maggior parte delle spiegazioni su push_back vs. emplace_back manca l’immagine completa.

L’anno scorso, ho tenuto una presentazione su C ++ Now on Type Deduction in C ++ 14 . emplace_back a parlare di push_back vs. emplace_back alle 13:49, ma ci sono informazioni utili che forniscono alcune prove a supporto prima di questo.

La vera differenza principale ha a che fare con costruttori impliciti contro espliciti. Considera il caso in cui abbiamo un singolo argomento che vogliamo passare a push_back o emplace_back .

 std::vector v; v.push_back(x); v.emplace_back(x); 

Dopo che il tuo ottimizzatore ha messo le mani su questo, non c’è differenza tra queste due affermazioni in termini di codice generato. La saggezza tradizionale è che push_back costruirà un object temporaneo, che verrà quindi spostato in v mentre emplace_back inoltrerà l’argomento e lo costruirà direttamente sul posto senza copie o spostamenti. Questo può essere vero in base al codice, come scritto nelle librerie standard, ma fa l’ipotesi errata che il lavoro del compilatore di ottimizzazione sia quello di generare il codice che hai scritto. Il compito del compilatore di ottimizzazione è in realtà di generare il codice che avresti scritto se fossi un esperto di ottimizzazioni specifiche della piattaforma e non si preoccupasse della manutenibilità, solo delle prestazioni.

La differenza effettiva tra queste due affermazioni è che il più potente emplace_back chiamerà qualsiasi tipo di costruttore là fuori, mentre il push_back più cauto chiamerà solo costruttori che sono impliciti. I costruttori impliciti dovrebbero essere sicuri. Se puoi build implicitamente una U da una T , stai dicendo che U può contenere tutte le informazioni in T senza perdita. In quasi tutte le situazioni è sicuro passare una T e nessuno se la farà diventare una U Un buon esempio di un costruttore implicito è la conversione da std::uint32_t a std::uint64_t . Un cattivo esempio di conversione implicita è double a std::uint8_t .

Vogliamo essere cauti nella nostra programmazione. Non vogliamo utilizzare funzionalità potenti perché più è potente la funzione, più è facile fare qualcosa in modo incorretto o imprevisto. Se intendi chiamare costruttori espliciti, allora hai bisogno del potere di emplace_back . Se si desidera chiamare solo costruttori impliciti, attenersi alla sicurezza di push_back .

Un esempio

 std::vector> v; T a; v.emplace_back(std::addressof(a)); // compiles v.push_back(std::addressof(a)); // fails to compile 

std::unique_ptr ha un costruttore esplicito da T * . Poiché emplace_back può chiamare costruttori espliciti, il passaggio di un puntatore non proprietario viene emplace_back . Tuttavia, quando v esce dall’ambito, il distruttore tenterà di chiamare delete su quel puntatore, che non è stato allocato da new perché è solo un object stack. Questo porta a comportamenti non definiti.

Questo non è solo un codice inventato. Questo è stato un vero bug di produzione che ho incontrato. Il codice era std::vector , ma possedeva i contenuti. Come parte della migrazione a C ++ 11, ho modificato correttamente T * in std::unique_ptr per indicare che il vettore possedeva la sua memoria. Tuttavia, stavo basando questi cambiamenti sulla mia comprensione nel 2012, durante il quale ho pensato “emplace_back fa tutto ciò che push_back può fare e altro, quindi perché dovrei mai usare push_back?”, Così ho anche cambiato il push_back in emplace_back .

Se avessi lasciato il codice come se avessi usato il push_back più sicuro, avrei immediatamente catturato questo bug di push_back data e sarebbe stato considerato un successo nell’aggiornamento a C ++ 11. Invece, ho mascherato il bug e non l’ho trovato fino a mesi dopo.

Compatibilità all’indietro con i compilatori pre-C ++ 11.

Alcune implementazioni della libreria di emplace_back non si comportano come specificato nello standard C ++ inclusa la versione fornita con Visual Studio 2012, 2013 e 2015.

Per far fronte a noti bug del compilatore, preferisci usare std::vector::push_back() se i parametri fanno riferimento a iteratori o altri oggetti che non saranno validi dopo la chiamata.

 std::vector v; v.emplace_back(123); v.emplace_back(v[0]); // Produces incorrect results in some compilers 

Su un compilatore, v contiene i valori 123 e 21 anziché il previsto 123 e 123. Ciò è dovuto al fatto che la seconda chiamata a emplace_back risulta in un ridimensionamento in cui il punto v[0] non è più valido.

Un’implementazione funzionante del codice precedente userebbe push_back() invece di emplace_back() come segue:

 std::vector v; v.emplace_back(123); v.push_back(v[0]); 

Nota: l’uso di un vettore di int è a scopo dimostrativo. Ho scoperto questo problema con una class molto più complessa che includeva variabili membro allocate dynamicmente e la chiamata a emplace_back() provocato un arresto anomalo.