¿Por qué usaría push_back en lugar de emplace_back?

¿Por qué usaría push_back en lugar de emplace_back?

He pensado bastante sobre 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 contra emplace_back te pierdas la imagen completa.

El año pasado, hice una presentación en C++Now sobre la deducción de tipos en C++14. Comienzo a hablar de push_back contra emplace_back a las 13:49, pero hay información útil que proporciona alguna evidencia de respaldo antes de eso.

La principal diferencia real tiene que ver con los constructores implícitos frente a los explícitos. Considere el caso en el que tenemos un solo argumento que queremos pasar a 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 se moverá a v mientras que emplace_back reenviará el argumento y lo construirá directamente en su lugar sin copias ni movimientos. Esto puede ser cierto según el código tal como está escrito en las bibliotecas estándar, pero asume erróneamente que el trabajo del compilador de optimización es generar el código que 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 plataforma y no se preocupara por la mantenibilidad, solo por el rendimiento.

La diferencia real entre estas dos afirmaciones es que el emplace_back más potente llamará a cualquier tipo de constructor, mientras que el push_back más cauteloso 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 U de un T , estás diciendo que U puede contener toda la información en T sin pérdida. Es seguro en casi cualquier situación pasar un T y a nadie le importará si lo convierte en un U en cambio. 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 cautos en nuestra programación. No queremos utilizar funciones potentes porque cuanto más potente sea la función, más fácil será hacer algo incorrecto o inesperado por accidente. Si tiene la intención de llamar a constructores explícitos, entonces necesita el poder de emplace_back . Si desea llamar solo a constructores implícitos, quédese 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 * . Porque emplace_back puede llamar a constructores explícitos, pasando un puntero no propietario compila bien. Sin embargo, cuando v sale del alcance, el destructor intentará llamar a delete en ese puntero, que no fue asignado por 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 poseía los contenidos. Como parte de la migración a C++11, cambié correctamente T * a std::unique_ptr<T> para indicar que el vector poseía su memoria. Sin embargo, basé estos cambios en mi entendimiento en 2012, durante el cual pensé "emplace_back hace todo lo que push_back puede hacer y más, entonces, ¿por qué usaría push_back?", así que también cambié el push_back a emplace_back .

Si hubiera dejado el código usando el push_back más seguro , 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, enmascaré el error y no lo encontré hasta meses después.


push_back siempre permite el uso de inicialización uniforme, que 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 compiladores anteriores a C++11.