Por que eu usaria push_back em vez de emplace_back?

232

Os vetores C ++ 11 têm a nova função emplace_back. Ao contrário push_back, que depende de otimizações do compilador para evitar cópias, emplace_backusa o encaminhamento perfeito para enviar os argumentos diretamente ao construtor para criar um objeto no local. Parece-me que emplace_backfaz tudo o que push_backpode fazer, mas algumas vezes o faz melhor (mas nunca pior).

Que motivo eu tenho que usar push_back?

David Stone
fonte

Respostas:

162

Eu pensei bastante sobre essa questão nos últimos quatro anos. Cheguei à conclusão de que a maioria das explicações sobre push_backvs. emplace_backperde 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_backvs. 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_backou 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_backconstruirá um objeto temporário, que será movido para a frente, venquanto emplace_backencaminhará 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_backchamará qualquer tipo de construtor, enquanto o mais cauteloso push_backchamará apenas construtores implícitos. Construtores implícitos devem ser seguros. Se você pode criar implicitamente um Ude a T, você está dizendo que Upode reter todas as informações Tsem perda. É seguro em praticamente qualquer situação passar um Te ninguém se importará se você o fizer U. Um bom exemplo de um construtor implícito é a conversão de std::uint32_tpara std::uint64_t. Um mau exemplo de uma conversão implícita é doublepara 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_backpode chamar construtores explícitos, passar um ponteiro não proprietário compila perfeitamente. No entanto, quando vfica fora do escopo, o destruidor tentará chamar deleteesse ponteiro, que não foi alocado por newser 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_backpara 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.

David Stone
fonte
Ajudaria se você pudesse explicar o que exatamente o lugar faz no seu exemplo e por que está errado.
eddi
4
@eddi: eu adicionei uma seção explicando isso: std::unique_ptr<T>tem um construtor explícito T *. Como emplace_backpode chamar construtores explícitos, passar um ponteiro não proprietário compila perfeitamente. No entanto, quando vfica fora do escopo, o destruidor tentará chamar deleteesse ponteiro, que não foi alocado por newser apenas um objeto de pilha. Isso leva a um comportamento indefinido.
David Stone
Obrigado por postar isso. Eu não sabia disso quando escrevi minha resposta, mas agora gostaria de escrever quando aprendi depois :) Quero muito dar um tapa nas pessoas que mudam para novos recursos apenas para fazer o que há de mais moderno. . Pessoal, as pessoas estavam usando C ++ antes do C ++ 11 também, e nem tudo era problemático. Se você não sabe por que está usando um recurso, não o use . Tão feliz que você postou isso e espero que fique muito mais positivo para que fique acima do meu. 1
user541686
1
@CaptainJacksparrow: Parece que eu digo implícito e explícito onde eu os digo. Qual parte você confundiu?
David Stone
4
@CaptainJacksparrow: um explicitconstrutor é um construtor que tem a palavra-chave explicitaplicada a ele. Um construtor "implícito" é qualquer construtor que não possua essa palavra-chave. No caso do std::unique_ptrconstrutor de T *, o implementador de std::unique_ptrescreveu esse construtor, mas o problema aqui é que o usuário desse tipo chamou emplace_back, que chamou esse construtor explícito. Se tivesse sido push_back, em vez de chamar esse construtor, teria se baseado em uma conversão implícita, que só pode chamar construtores implícitos.
David Stone
117

push_backsempre permite o uso de inicialização uniforme, da qual gosto muito. Por exemplo:

struct aggregate {
    int foo;
    int bar;
};

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

Por outro lado, v.emplace_back({ 42, 121 });não vai funcionar.

Luc Danton
fonte
58
Observe que isso se aplica apenas à inicialização agregada e à inicialização da lista de inicializadores. Se você usasse a {}sintaxe para chamar um construtor real, basta remover os {}'e usar emplace_back.
Nicol Bolas
Tempo mudo de pergunta: então emplace_back não pode ser usado para vetores de estruturas? Ou apenas não para esse estilo usando literal {42.121}?
Phil H
1
@ LucDanton: Como eu disse, isso só se aplica à agregação e inicialização da lista de inicializadores. Você pode usar a {}sintaxe para chamar construtores reais. Você poderia dar aggregateum 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_backseria preferível, pois ele chama o construtor no local. E, portanto, não requer que o tipo seja copiável.
Nicol Bolas
11
Isso foi visto como um defeito no padrão e foi resolvido. Veja cplusplus.github.io/LWG/lwg-active.html#2089
David Stone
3
@ DavidStone Se tivesse sido resolvido, ainda não estaria na lista "ativa" ... não? Parece continuar sendo uma questão pendente. A atualização mais recente, intitulada " [2018-08-23 Batavia Issues processing] ", diz que " P0960 (atualmente em vôo) deve resolver isso. " E ainda não consigo compilar código que tenta emplaceagregar 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.
underscore_d
80

Compatibilidade com versões anteriores com compiladores anteriores ao C ++ 11.

user541686
fonte
21
Essa parece ser a maldição do C ++. Temos vários recursos interessantes a cada novo lançamento, mas muitas empresas estão presas usando alguma versão antiga para fins de compatibilidade ou desencorajando (se não proibindo) o uso de determinados recursos.
9788 Dan Albert Albert
6
@Mehrdad: Por que se contentar com o suficiente quando você pode se divertir? Eu com certeza não gostaria de programar em blub , mesmo que fosse suficiente. Não estou dizendo que é o caso deste exemplo em particular, mas como alguém que passa a maior parte do tempo programando em C89 por uma questão de compatibilidade, é definitivamente um problema real.
Dan Albert
3
Eu não acho que isso seja realmente uma resposta para a pergunta. Para mim, ele está solicitando casos de uso onde push_backé preferível.
Boy
3
@ Mr.Boy: É preferível quando você quer ser compatível com versões anteriores dos compiladores anteriores ao C ++ 11. Isso não ficou claro na minha resposta?
user541686
6
Isso recebeu muito mais atenção do que eu esperava, portanto, para todos vocês lendo isto: nãoemplace_back é uma versão "ótima" do . É uma versão potencialmente perigosa . Leia as outras respostas. push_back
user541686
68

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.

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

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_backresultar em um redimensionamento no qual o ponto v[0]se torna inválido.

Uma implementação de trabalho do código acima usaria em push_back()vez do emplace_back()seguinte:

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

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.

Marc
fonte
Esperar. Isso parece ser um bug. Como pode push_backser diferente neste caso?
balki
12
A chamada para emplace_back () usa o encaminhamento perfeito para executar a construção no local e, como tal, v [0] não é avaliada até após o redimensionamento do vetor (no ponto em que v [0] é inválido). push_back constrói o novo elemento e copia / move o elemento conforme necessário e v [0] é avaliado antes de qualquer realocação.
Marc
1
@ Marc: É garantido pelo padrão que o emplace_back funciona mesmo para elementos dentro da faixa.
David Stone
1
@DavidStone: Por favor, poderia fornecer uma referência de onde no padrão esse comportamento é garantido? De qualquer forma, o Visual Studio 2012 e 2015 exibem um comportamento incorreto.
Marc
4
@cameino: emplace_back existe para atrasar a avaliação de seu parâmetro para reduzir a cópia desnecessária. O comportamento é indefinido ou um bug do compilador (análise pendente do padrão). Recentemente, executei o mesmo teste no Visual Studio 2015 e obtive 123,3 na versão x64, 123,40 na versão Win32 e 123, -572662307 em Debug x64 e Debug Win32.
Marc