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.
std::unique_ptr<T>
tem um construtor explícitoT *
. Comoemplace_back
pode chamar construtores explícitos, passar um ponteiro não proprietário compila perfeitamente. No entanto, quandov
fica fora do escopo, o destruidor tentará chamardelete
esse ponteiro, que não foi alocado pornew
ser apenas um objeto de pilha. Isso leva a um comportamento indefinido.explicit
construtor é um construtor que tem a palavra-chaveexplicit
aplicada a ele. Um construtor "implícito" é qualquer construtor que não possua essa palavra-chave. No caso dostd::unique_ptr
construtor deT *
, o implementador destd::unique_ptr
escreveu esse construtor, mas o problema aqui é que o usuário desse tipo chamouemplace_back
, que chamou esse construtor explícito. Se tivesse sidopush_back
, em vez de chamar esse construtor, teria se baseado em uma conversão implícita, que só pode chamar construtores implícitos.push_back
sempre permite o uso de inicialização uniforme, da qual gosto muito. Por exemplo:Por outro lado,
v.emplace_back({ 42, 121 });
não vai funcionar.fonte
{}
sintaxe para chamar um construtor real, basta remover os{}
'e usaremplace_back
.{}
sintaxe para chamar construtores reais. Você poderia daraggregate
um construtor que usa 2 números inteiros, e esse construtor seria chamado ao usar a{}
sintaxe. O ponto é que, se você estiver tentando chamar um construtor,emplace_back
seria preferível, pois ele chama o construtor no local. E, portanto, não requer que o tipo seja copiável.emplace
agregar sem escrever explicitamente um construtor padrão . Também não está claro neste momento se será tratado como um defeito e, portanto, elegível para backporting ou se os usuários de C ++ <20 permanecerão SoL.Compatibilidade com versões anteriores com compiladores anteriores ao C ++ 11.
fonte
push_back
é preferível.emplace_back
é uma versão "ótima" do . É uma versão potencialmente perigosa . Leia as outras respostas.push_back
Algumas implementações de biblioteca de emplace_back não se comportam conforme especificado no padrão C ++, incluindo a versão que acompanha o Visual Studio 2012, 2013 e 2015.
Para acomodar erros conhecidos do compilador, prefira usar
std::vector::push_back()
se os parâmetros fizerem referência a iteradores ou outros objetos que serão inválidos após a chamada.Em um compilador, v contém os valores 123 e 21 em vez dos 123 e 123 esperados. Isso ocorre devido ao fato de a segunda chamada
emplace_back
resultar em um redimensionamento no qual o pontov[0]
se torna inválido.Uma implementação de trabalho do código acima usaria em
push_back()
vez doemplace_back()
seguinte:Nota: O uso de um vetor de ints é para fins de demonstração. Descobri esse problema com uma classe muito mais complexa, que incluía variáveis de membro alocadas dinamicamente e a chamada para
emplace_back()
resultar em uma falha grave.fonte
push_back
ser diferente neste caso?