¿Por qué debería usar push_back en lugar de emplace_back?
Los vectores C++ 11 tienen la nueva función emplace_back
. A diferencia de push_back
, que se basa en optimizaciones del compilador para evitar copias, emplace_back
utiliza el reenvío perfecto para enviar los argumentos directamente al constructor para crear un objeto in situ. Me parece que lo emplace_back
hace todo.push_back
que puede hacer, pero algunas veces lo hará mejor (pero nunca peor).
¿Qué motivo tengo para utilizar push_back
?
He pensado bastante en esta pregunta durante los últimos cuatro años. He llegado a la conclusión de que la mayoría de las explicaciones sobre push_back
vs.emplace_back
pierden la imagen completa.
El año pasado, hice una presentación en C++Now sobre la deducción de tipos en C++14 . Empiezo a hablar de push_back
vs.emplace_back
a las 13:49, pero hay información útil que proporciona alguna evidencia de respaldo antes de eso.
La verdadera diferencia principal tiene que ver con los constructores implícitos y explícitos. Considere el caso en el que tenemos un único argumento al que queremos pasar push_back
o emplace_back
.
std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Después de que su compilador de optimización tenga esto en sus manos, no hay diferencia entre estas dos declaraciones en términos de código generado. La sabiduría tradicional es que push_back
construirá un objeto temporal, que luego será trasladado v
mientrasemplace_back
reenviará el argumento y lo construirá directamente en su lugar sin copias ni movimientos. Esto puede ser cierto según el código escrito en las bibliotecas estándar, pero supone erróneamente que el trabajo del compilador de optimización es generar el código que usted escribió. El trabajo del compilador de optimización es en realidad generar el código que habría escrito si fuera un experto en optimizaciones específicas de plataformas y no le importara la mantenibilidad, solo el rendimiento.
La diferencia real entre estas dos declaraciones es que las más poderosas emplace_back
llamarán a cualquier tipo de constructor, mientras que las más cautelosas push_back
llamarán sólo a los constructores que estén implícitos. Se supone que los constructores implícitos son seguros. Si puede construir implícitamente a a U
partir de a T
, está diciendo que U
puede contener toda la información T
sin pérdida. Es seguro en casi cualquier situación pasar un T
y a nadie le importará si lo haces U
en su lugar. Un buen ejemplo de constructor implícito es la conversión de std::uint32_t
a std::uint64_t
. Un mal ejemplo de conversión implícita es double
a std::uint8_t
.
Queremos ser cautelosos en nuestra programación. No queremos utilizar funciones potentes porque cuanto más potentes sean, más fácil será hacer accidentalmente algo incorrecto o inesperado. Si tiene la intención de llamar a constructores explícitos, entonces necesita el poder de emplace_back
. Si desea llamar sólo a constructores implícitos, siga con la seguridad de push_back
.
Un ejemplo
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>
tiene un constructor explícito de T *
. Debido a que emplace_back
puede llamar a constructores explícitos, pasar un puntero que no sea propietario se compila bien. Sin embargo, cuando v
sale del alcance, el destructor intentará llamar delete
a ese puntero, que no fue asignado new
porque es solo un objeto de pila. Esto conduce a un comportamiento indefinido.
Esto no es sólo un código inventado. Este fue un error de producción real que encontré. El código era std::vector<T *>
, pero era dueño del contenido. Como parte de la migración a C++11, cambié correctamente T *
a std::unique_ptr<T>
para indicar que el vector era dueño de su memoria. Sin embargo, estaba basando estos cambios en mi comprensión de 2012, durante el cual pensé " emplace_back
todo se push_back
puede hacer y más, entonces, ¿por qué debería usarlo push_back
?", así que también cambié push_back
a emplace_back
.
Si, en cambio, hubiera dejado el código usando el modo más seguro push_back
, habría detectado instantáneamente este error de larga data y se habría visto como un éxito de la actualización a C++11. En cambio, oculté el error y no lo encontré hasta meses después.
push_back
siempre permite el uso de inicialización uniforme, lo cual me gusta mucho. Por ejemplo:
struct aggregate {
int foo;
int bar;
};
std::vector<aggregate> v;
v.push_back({ 42, 121 });
Por otro lado, v.emplace_back({ 42, 121 });
no funcionará.
Compatibilidad con versiones anteriores de compiladores anteriores a C++11.
Algunas implementaciones de biblioteca de emplace_back no se comportan según lo especificado en el estándar C++, incluida la versión que se incluye con Visual Studio 2012, 2013 y 2015.
Para dar cabida a errores conocidos del compilador, prefiera usarlo std::vector::push_back()
si los parámetros hacen referencia a iteradores u otros objetos que no serán válidos después de la llamada.
std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers
En un compilador, v contiene los valores 123 y 21 en lugar de los 123 y 123 esperados. Esto se debe al hecho de que la segunda llamada a emplace_back
da como resultado un cambio de tamaño, momento en el cual v[0]
deja de ser válido.
Una implementación funcional del código anterior usaría push_back()
en lugar de emplace_back()
lo siguiente:
std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);
Nota: El uso de un vector de enteros es para fines de demostración. Descubrí este problema con una clase mucho más compleja que incluía variables miembro asignadas dinámicamente y la llamada a emplace_back()
resultó en un bloqueo grave.