He pensado bastante en esta pregunta en los últimos cuatro años. He llegado a la conclusión de que la mayoría de las explicaciones sobre push_back
vs. se emplace_back
pierden la imagen completa.
El año pasado, hice una presentación en C ++ Now sobre Type Deduction en C ++ 14 . Empiezo a hablar sobre push_back
vs. emplace_back
a las 13:49, pero hay información útil que proporciona alguna evidencia de apoyo antes de eso.
La diferencia principal real tiene que ver con constructores implícitos versus 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 se haga cargo de esto, 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 se moverá hacia adentro, v
mientras emplace_back
que reenviará el argumento y lo construirá directamente en su lugar sin copias ni movimientos. Esto puede ser cierto en función del código tal como está escrito en las bibliotecas estándar, pero supone erróneamente que el trabajo del compilador de optimización es generar el código que escribió. El trabajo del compilador optimizador es en realidad generar el código que habría escrito si fuera un experto en optimizaciones específicas de la plataforma y no le importara la mantenibilidad, solo el rendimiento.
La diferencia real entre estas dos afirmaciones es que el más poderoso emplace_back
llamará a cualquier tipo de constructor, mientras que el más cauteloso push_back
llamará solo a los constructores que están implícitos. Se supone que los constructores implícitos son seguros. Si puede construir implícitamente un 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 en su U
lugar. Un buen ejemplo de un constructor implícito es la conversión de std::uint32_t
a std::uint64_t
. Un mal ejemplo de una 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 potente es la función, más fácil es 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 solo desea llamar a constructores implícitos, cumpla 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 no propietario compila perfectamente. Sin embargo, cuando está v
fuera de alcance, el destructor intentará llamar delete
a ese puntero, que no fue asignado new
porque es solo un objeto de pila. Esto lleva a un comportamiento indefinido.
Este no es solo un código inventado. Este fue un error de producción real que encontré. El código era std::vector<T *>
, pero poseía los contenidos. Como parte de la migración a C ++ 11, I correctamente cambiado T *
a std::unique_ptr<T>
indicar que el vector poseía su memoria. Sin embargo, basé estos cambios en mi comprensión en 2012, durante el cual pensé "emplace_back hace todo lo que push_back puede hacer y más, así que ¿por qué usaría push_back?", Así que también cambié push_back
a emplace_back
.
Si hubiera dejado el código como el más seguro push_back
, habría detectado instantáneamente este error de larga data y habría sido visto como un éxito al actualizar a C ++ 11. En cambio, enmascaré el error y no lo encontré hasta meses después.