Perché dovrei mai usare push_back invece di emplace_back?


232

I vettori C ++ 11 hanno la nuova funzione emplace_back. Diversamente push_back, che si basa sulle ottimizzazioni del compilatore per evitare copie, emplace_backutilizza l'inoltro perfetto per inviare gli argomenti direttamente al costruttore per creare un oggetto sul posto. Mi sembra che emplace_backfa tutto ciò che push_backpuò fare, ma qualche volta lo farà meglio (ma mai peggio).

Quale motivo devo usare push_back?

Risposte:


162

Ho pensato a questa domanda un bel po 'negli ultimi quattro anni. Sono giunto alla conclusione che la maggior parte delle spiegazioni su push_backvs. emplace_backmanca il quadro completo.

L'anno scorso ho tenuto una presentazione al C ++ Now on Type Deduction in C ++ 14 . Comincio a parlare push_backcontro emplace_backalle 13:49, ma ci sono informazioni utili che forniscono alcune prove a supporto prima di quello.

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

std::vector<T> v;
v.push_back(x);
v.emplace_back(x);

Dopo che il compilatore ottimizzatore ha messo le mani su questo, non c'è alcuna differenza tra queste due istruzioni in termini di codice generato. La saggezza tradizionale è che push_backcostruirà un oggetto temporaneo, nel quale verrà spostato, vmentre emplace_backavanzerà l'argomento e lo costruirà direttamente in posizione senza copie o mosse. 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 lavoro del compilatore di ottimizzazione è in realtà quello di generare il codice che avresti scritto se tu fossi un esperto di ottimizzazioni specifiche della piattaforma e non ti occupassi della manutenibilità, ma solo delle prestazioni.

La differenza effettiva tra queste due affermazioni è che più potente emplace_backchiamerà qualsiasi tipo di costruttore là fuori, mentre più cauto push_backchiamerà solo costruttori che sono impliciti. I costruttori impliciti dovrebbero essere sicuri. Se puoi implicitamente costruire un Uda un T, stai dicendo che Upuò contenere tutte le informazioni Tsenza alcuna perdita. È sicuro in quasi ogni situazione passare un Te nessuno si preoccuperà se lo fai Uinvece. Un buon esempio di costruttore implicito è la conversione da std::uint32_ta std::uint64_t. Un cattivo esempio di una conversione implicita è doublea std::uint8_t.

Vogliamo essere cauti nella nostra programmazione. Non vogliamo utilizzare funzionalità potenti perché più potente è la funzionalità, più facile è fare accidentalmente qualcosa di errato o imprevisto. Se hai intenzione di chiamare costruttori espliciti, allora hai bisogno del potere di emplace_back. Se vuoi chiamare solo costruttori impliciti, mantieni la sicurezza di push_back.

Un esempio

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

std::unique_ptr<T>ha un costruttore esplicito da T *. Poiché emplace_backpuò chiamare costruttori espliciti, il passaggio di un puntatore non proprietario viene compilato correttamente. Tuttavia, quando vesce dall'ambito, il distruttore tenterà di richiamare deletequel puntatore, che non è stato allocato newperché è solo un oggetto stack. Questo porta a un comportamento indefinito.

Questo non è solo un codice inventato. Questo è stato un vero errore di produzione che ho riscontrato. Il codice era std::vector<T *>, ma possedeva il contenuto. Come parte della migrazione di C ++ 11, ho correttamente cambiato T *per std::unique_ptr<T>indicare che il vettore possedeva sua memoria. Tuttavia, mi è stato basando questi cambiamenti dalla mia comprensione nel 2012, durante il quale ho pensato "emplace_back fa di tutto push_back può fare di più, e allora perché mai dovrei usare push_back?", Così ho anche cambiato il push_backa emplace_back.

Se invece avessi lasciato il codice come usando il più sicuro push_back, avrei immediatamente rilevato questo bug di vecchia data e sarebbe stato visto come un successo dell'aggiornamento a C ++ 11. Invece, ho mascherato il bug e non l'ho trovato fino a mesi dopo.


Sarebbe di aiuto se tu potessi approfondire cosa fa esattamente emplace nel tuo esempio e perché è sbagliato.
eddi,

4
@eddi: ho aggiunto una sezione che spiega questo: std::unique_ptr<T>ha un costruttore esplicito da T *. Poiché emplace_backpuò chiamare costruttori espliciti, il passaggio di un puntatore non proprietario viene compilato correttamente. Tuttavia, quando vesce dall'ambito, il distruttore tenterà di richiamare deletequel puntatore, che non è stato allocato newperché è solo un oggetto stack. Questo porta a un comportamento indefinito.
David Stone,

Grazie per aver pubblicato questo Non lo sapevo quando ho scritto la mia risposta, ma ora vorrei averlo scritto da solo quando l'ho imparato in seguito :) Voglio davvero schiaffeggiare le persone che passano a nuove funzionalità solo per fare la cosa più alla moda che riescono a trovare . Ragazzi, le persone usavano anche C ++ prima di C ++ 11 e non tutto era problematico. Se non sai perché stai utilizzando una funzione, non utilizzarla . Sono contento che tu l'abbia pubblicato e spero che ottenga molti più voti, quindi vada oltre il mio. +1
user541686

1
@CaptainJacksparrow: sembra che dico implicito ed esplicito dove intendo. Quale parte hai confuso?
David Stone,

4
@CaptainJacksparrow: un explicitcostruttore è un costruttore a cui è stata explicitapplicata la parola chiave . Un costruttore "implicito" è qualsiasi costruttore che non ha quella parola chiave. Nel caso del std::unique_ptrcostruttore di T *, l'implementatore di ha std::unique_ptrscritto quel costruttore, ma il problema qui è che l'utente di quel tipo ha chiamato emplace_back, che ha chiamato quel costruttore esplicito. Se lo fosse stato push_back, invece di chiamare quel costruttore, avrebbe fatto affidamento su una conversione implicita, che può solo chiamare costruttori impliciti.
David Stone,

117

push_backconsente sempre l'uso dell'inizializzazione uniforme, di cui sono molto affezionato. Per esempio:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

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


58
Si noti che ciò si applica solo all'inizializzazione aggregata e all'inizializzazione dell'elenco di inizializzatori. Se utilizzi la {}sintassi per chiamare un costruttore reale, puoi semplicemente rimuovere {}'s e usare emplace_back.
Nicol Bolas,

Tempo di domande stupido: quindi emplace_back non può essere usato per i vettori di strutture? O semplicemente non per questo stile usando letterale {42,121}?
Phil H,

1
@LucDanton: come ho detto, si applica solo all'inizializzazione dell'elenco di inizializzatori e aggregati . È possibile utilizzare la sintassi per chiamare costruttori effettivi. Si potrebbe dare un costruttore che impiega 2 numeri interi e questo costruttore verrebbe chiamato quando si usa la sintassi. Il punto è che se stai cercando di chiamare un costruttore, sarebbe preferibile, dal momento che chiama il costruttore sul posto. E quindi non richiede che il tipo sia copiabile. {}aggregate{}emplace_back
Nicol Bolas,

11
Questo è stato visto come un difetto nello standard ed è stato risolto. Vedi cplusplus.github.io/LWG/lwg-active.html#2089
David Stone,

3
@DavidStone Se fosse stato risolto, non sarebbe ancora nell'elenco "attivo" ... no? Sembra rimanere un problema in sospeso. L'ultimo aggiornamento, intitolato " [2018-08-23 Batavia Issues processing] ", afferma che " P0960 (attualmente in volo) dovrebbe risolvere questo problema. " E non riesco ancora a compilare il codice che tenta di emplaceaggregare senza scrivere esplicitamente un costruttore di plateplate . Non è ancora chiaro a questo punto se verrà trattato come un difetto e quindi idoneo per il backporting o se gli utenti di C ++ <20 rimarranno SoL.
underscore_d

80

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


21
Questa sembra essere la maledizione del C ++. Otteniamo tonnellate di funzioni interessanti con ogni nuova versione, ma molte aziende sono bloccate a utilizzare una versione precedente per motivi di compatibilità o per scoraggiare (se non vietare) l'uso di determinate funzionalità.
Dan Albert,

6
@Mehrdad: Perché accontentarsi di quando puoi avere grandi cose? Non vorrei certo programmare in blub , anche se fosse sufficiente. Non dire che è il caso di questo esempio in particolare, ma come qualcuno che trascorre la maggior parte del suo tempo a programmare in C89 per motivi di compatibilità, è sicuramente un vero problema.
Dan Albert,

3
Non penso che questa sia davvero una risposta alla domanda. Per me sta chiedendo casi d'uso dove push_backè preferibile.
Mr. Boy,

3
@ Mr.Boy: è preferibile quando si desidera essere retrocompatibili con i compilatori pre-C ++ 11. Non era chiaro nella mia risposta?
user541686,

6
Questo ha ottenuto molta più attenzione di quanto mi aspettassi, quindi per tutti voi che leggete questo: nonemplace_back è una versione "fantastica" di . È una versione potenzialmente pericolosa di esso. Leggi le altre risposte. push_back
user541686

68

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 soddisfare i bug noti del compilatore, preferisci utilizzare std::vector::push_back()se i parametri fanno riferimento a iteratori o altri oggetti che non saranno validi dopo la chiamata.

std::vector<int> 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 invece dei 123 e 123 previsti. Ciò è dovuto al fatto che la seconda chiamata a emplace_backrisultati in un ridimensionamento a quel punto v[0]diventa non valida.

Un'implementazione funzionante del codice sopra dovrebbe utilizzare push_back()invece emplace_back()come segue:

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

Nota: l'uso di un vettore di ints è a scopo dimostrativo. Ho scoperto questo problema con una classe molto più complessa che includeva variabili membro allocate dinamicamente e la chiamata a emplace_back()provocare un arresto anomalo.


Aspettare. Questo sembra essere un bug. Come può push_backessere diverso in questo caso?
Balki,

12
La chiamata a emplace_back () usa l'inoltro perfetto per eseguire la costruzione sul posto e come tale v [0] non viene valutato fino a dopo il ridimensionamento del vettore (a quel punto v [0] non è valido). push_back costruisce il nuovo elemento e copia / sposta l'elemento come necessario e v [0] viene valutato prima di qualsiasi riallocazione.
Marc

1
@Marc: è garantito dallo standard che emplace_back funziona anche per elementi all'interno dell'intervallo.
David Stone,

1
@DavidStone: Potrebbe fornire un riferimento a dove nello standard è garantito questo comportamento? In entrambi i casi, Visual Studio 2012 e 2015 mostrano un comportamento errato.
Marc

4
@cameino: emplace_back esiste per ritardare la valutazione del suo parametro per ridurre le copie non necessarie. Il comportamento non è definito o è un bug del compilatore (in attesa dell'analisi dello standard). Di recente ho eseguito lo stesso test su Visual Studio 2015 e ho ottenuto 123,3 con la versione x64, 123,40 con la versione Win32 e 123, -572662307 con Debug x64 e Debug Win32.
Marc
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.