Introdução: por que isso é importante?
\n\nVocê já se perguntou por que existem duas maneiras de adicionar um elemento ao final de um vector em C++? Afinal, por que reinventar a roda se existe o já consolidado push_back? A resposta está no desempenho e na flexibilidade do C++ moderno.
No Stack Overflow, essa pergunta acumulou milhares de visualizações e centenas de comentários. E não é por acaso: entender a diferença entre push_back e emplace_back não é apenas um conhecimento acadêmico, mas uma habilidade prática que pode acelerar seu código em 2 a 3 vezes. Vamos entender de uma vez por todas.
Diferença básica: o que dizem as documentações?
\n\nVamos começar pelo simples. Ambos os métodos adicionam um elemento ao final do contêiner (vector, list, queue). Mas eles fazem isso de maneiras diferentes:
\n\n- \n
push_back— recebe um objeto pronto (por valor, referência ou referência rvalue) e copia/move esse objeto para o contêiner. \n emplace_back— recebe os argumentos para o construtor do objeto e cria o objeto diretamente dentro do contêiner (construção in-place). \n
Parece abstrato? Vamos ver o código.
\n\nExemplo prático: criando uma classe
\n\nVamos imaginar que temos uma classe simples Person:
#include <iostream>\n#include <vector>\n#include <string>\n\nclass Person {\npublic:\n std::string name;\n int age;\n\n // Construtor\n Person(const std::string& n, int a) \n : name(n), age(a) {\n std::cout << "Construtor chamado para " << name << std::endl;\n }\n\n // Construtor de cópia\n Person(const Person& other) \n : name(other.name), age(other.age) {\n std::cout << "Construtor de cópia para " << name << std::endl;\n }\n\n // Construtor de movimento\n Person(Person&& other) noexcept\n : name(std::move(other.name)), age(other.age) {\n std::cout << "Construtor de movimento para " << name << std::endl;\n }\n};\n\nAgora vamos ver o que acontece ao usar diferentes métodos.
\n\nOpção 1: push_back com objeto temporário
\n\nint main() {\n std::vector<Person> people;\n people.push_back(Person("Alice", 25));\n return 0;\n}\n\nSaída:
\nConstrutor chamado para Alice\nConstrutor de movimento para Alice\n\nO que aconteceu? Primeiro, um objeto temporário Person é criado (primeira chamada do construtor). Em seguida, esse objeto é movido para o vector (construtor de movimento). Duas chamadas — trabalho em dobro.
Opção 2: push_back com objeto pronto
\n\nint main() {\n std::vector<Person> people;\n Person alice("Alice", 25);\n people.push_back(alice); // alice — lvalue\n return 0;\n}\n\nSaída:
\nConstrutor chamado para Alice\nConstrutor de cópia para Alice\n\nAqui vemos uma cópia, não um movimento. Se o objeto for grande (contendo, por exemplo, arrays dinâmicos), a cópia pode ser cara.
\n\nOpção 3: emplace_back
\n\nint main() {\n std::vector<Person> people;\n people.emplace_back("Bob", 30); // Passamos os argumentos para o construtor\n return 0;\n}\n\nSaída:
\nConstrutor chamado para Bob\n\nApenas uma chamada de construtor! O objeto Person é criado diretamente dentro da memória do vector, sem cópias ou movimentos intermediários.
Desempenho: quando a diferença é crítica?
\n\nPara tipos simples (int, double, ponteiros), a diferença é imperceptível — o compilador otimiza tudo. Mas para objetos complexos, a diferença pode ser significativa:
\n\n- \n
- Objetos com construtor de cópia pesado (por exemplo,
std::string, contêineres