Introducción: ¿por qué es importante?
\n\n¿Alguna vez te has preguntado por qué en C++ existen dos formas de agregar un elemento al final de un vector? Parecería, ¿para qué reinventar la rueda si existe el probado push_back? La respuesta está en el rendimiento y la flexibilidad del C++ moderno.
En Stack Overflow, esta pregunta ha acumulado miles de visitas y cientos de comentarios. Y no es casualidad: entender la diferencia entre push_back y emplace_back no es solo un conocimiento académico, sino una habilidad práctica que puede acelerar tu código de 2 a 3 veces. Analicémoslo de una vez por todas.
Diferencia básica: ¿qué dicen las documentaciones?
\n\nEmpecemos con lo simple. Ambos métodos agregan un elemento al final del contenedor (vector, lista, cola). Pero lo hacen de manera diferente:
\n\n- \n
push_back— recibe un objeto ya creado (por valor, referencia o referencia rvalue) y lo copia/mueve al contenedor. \n emplace_back— recibe los argumentos para el constructor del objeto y lo crea directamente dentro del contenedor (construcción in-place). \n
¿Suena abstracto? Veamos el código.
\n\nEjemplo práctico: creando una clase
\n\nImaginemos que tenemos una clase simple Person:
#include <iostream>\n#include <vector>\n#include <string>\n\nclass Person {\npublic:\n std::string name;\n int age;\n\n // Constructor\n Person(const std::string& n, int a) \n : name(n), age(a) {\n std::cout << "Constructor llamado para " << name << std::endl;\n }\n\n // Constructor de copia\n Person(const Person& other) \n : name(other.name), age(other.age) {\n std::cout << "Constructor de copia para " << name << std::endl;\n }\n\n // Constructor de movimiento\n Person(Person&& other) noexcept\n : name(std::move(other.name)), age(other.age) {\n std::cout << "Constructor de movimiento para " << name << std::endl;\n }\n};\n\nAhora veamos qué sucede al usar diferentes métodos.
\n\nOpción 1: push_back con objeto temporal
\n\nint main() {\n std::vector<Person> people;\n people.push_back(Person("Alice", 25));\n return 0;\n}\n\nSalida:
\nConstructor llamado para Alice\nConstructor de movimiento para Alice\n\n¿Qué sucedió? Primero se crea un objeto temporal Person (primera llamada al constructor). Luego, este objeto se mueve al vector (constructor de movimiento). Dos llamadas: trabajo duplicado.
Opción 2: push_back con objeto ya creado
\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\nSalida:
\nConstructor llamado para Alice\nConstructor de copia para Alice\n\nAquí vemos una copia, no un movimiento. Si el objeto es grande (contiene, por ejemplo, arreglos dinámicos), la copia puede ser costosa.
\n\nOpción 3: emplace_back
\n\nint main() {\n std::vector<Person> people;\n people.emplace_back("Bob", 30); // Pasamos argumentos para el constructor\n return 0;\n}\n\nSalida:
\nConstructor llamado para Bob\n\n¡Solo una llamada al constructor! El objeto Person se crea directamente dentro de la memoria del vector, sin copias o movimientos intermedios.
Rendimiento: ¿cuándo es crítica la diferencia?
\n\nPara tipos simples (int, double, punteros) la diferencia es imperceptible: el compilador lo optimiza todo. Pero para objetos complejos, la diferencia puede ser significativa:
\n\n- \n
- Objetos con constructor de copia pesado (por ejemplo,
std::string, contenedo