Eu pensei bastante sobre essa questão nos últimos quatro anos. Cheguei à conclusão de que a maioria das explicações sobre push_back
vs. emplace_back
perde a imagem completa.
No ano passado, fiz uma apresentação no C ++ Now sobre dedução de tipo no C ++ 14 . Começo a falar sobre push_back
vs. emplace_back
às 13:49, mas há informações úteis que fornecem algumas evidências de suporte antes disso.
A diferença principal real tem a ver com construtores implícitos vs. explícitos. Considere o caso em que temos um único argumento que queremos passar para push_back
ou emplace_back
.
std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Após o compilador de otimização colocar as mãos nisso, não há diferença entre essas duas instruções em termos de código gerado. A sabedoria tradicional é que push_back
construirá um objeto temporário, que será movido para a frente, v
enquanto emplace_back
encaminhará o argumento e o construirá diretamente no lugar, sem cópias ou movimentos. Isso pode ser verdade com base no código escrito nas bibliotecas padrão, mas assume-se erroneamente que o trabalho do compilador otimizador é gerar o código que você escreveu. O trabalho do compilador de otimização é realmente gerar o código que você escreveria se fosse um especialista em otimizações específicas da plataforma e não se importasse com a manutenção, apenas com o desempenho.
A diferença real entre essas duas instruções é que quanto mais poderoso emplace_back
chamará qualquer tipo de construtor, enquanto o mais cauteloso push_back
chamará apenas construtores implícitos. Construtores implícitos devem ser seguros. Se você pode criar implicitamente um U
de a T
, você está dizendo que U
pode reter todas as informações T
sem perda. É seguro em praticamente qualquer situação passar um T
e ninguém se importará se você o fizer U
. Um bom exemplo de um construtor implícito é a conversão de std::uint32_t
para std::uint64_t
. Um mau exemplo de uma conversão implícita é double
para std::uint8_t
.
Queremos ser cautelosos em nossa programação. Não queremos usar recursos poderosos, porque quanto mais poderoso o recurso, mais fácil é acidentalmente fazer algo incorreto ou inesperado. Se você pretende chamar construtores explícitos, precisará do poder de emplace_back
. Se você quiser chamar apenas construtores implícitos, mantenha a segurança de push_back
.
Um exemplo
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>
tem um construtor explícito de T *
. Como emplace_back
pode chamar construtores explícitos, passar um ponteiro não proprietário compila perfeitamente. No entanto, quando v
fica fora do escopo, o destruidor tentará chamar delete
esse ponteiro, que não foi alocado por new
ser apenas um objeto de pilha. Isso leva a um comportamento indefinido.
Este não é apenas um código inventado. Este foi um erro de produção real que encontrei. O código era std::vector<T *>
, mas possuía o conteúdo. Como parte da migração para C ++ 11, eu corretamente mudou T *
para std::unique_ptr<T>
indicar que o vector de propriedade sua memória. No entanto, eu estava baseando essas mudanças no meu entendimento em 2012, durante o qual pensei "emplace_back faz tudo que push_back pode fazer e muito mais, então por que eu usaria push_back?", Então mudei também push_back
para emplace_back
.
Se eu tivesse deixado o código usando o mais seguro push_back
, instantaneamente teria detectado esse bug de longa data e teria sido visto como um sucesso da atualização para o C ++ 11. Em vez disso, mascarei o bug e não o encontrei meses depois.