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

Resuelto David Stone asked hace 12 años • 6 respuestas

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_backutiliza el reenvío perfecto para enviar los argumentos directamente al constructor para crear un objeto in situ. Me parece que lo emplace_backhace todo.push_back que puede hacer, pero algunas veces lo hará mejor (pero nunca peor).

¿Qué motivo tengo para utilizar push_back?

David Stone avatar Jun 05 '12 09:06 David Stone
Aceptado

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_backvs.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_backvs.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_backo 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_backconstruirá un objeto temporal, que luego será trasladado vmientrasemplace_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_backllamarán a cualquier tipo de constructor, mientras que las más cautelosas push_backllamará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 Upartir de a T, está diciendo que Upuede contener toda la información Tsin pérdida. Es seguro en casi cualquier situación pasar un Ty a nadie le importará si lo haces Uen su lugar. Un buen ejemplo de constructor implícito es la conversión de std::uint32_ta std::uint64_t. Un mal ejemplo de conversión implícita es doublea 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_backpuede llamar a constructores explícitos, pasar un puntero que no sea propietario se compila bien. Sin embargo, cuando vsale del alcance, el destructor intentará llamar deletea ese puntero, que no fue asignado newporque 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_backtodo se push_backpuede hacer y más, entonces, ¿por qué debería usarlo push_back?", así que también cambié push_backa 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.

David Stone avatar Apr 28 '2016 15:04 David Stone

push_backsiempre 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á.

Luc Danton avatar Jun 05 '2012 02:06 Luc Danton

Compatibilidad con versiones anteriores de compiladores anteriores a C++11.

user541686 avatar Jun 05 '2012 02:06 user541686

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_backda 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.

Marc avatar Feb 10 '2015 15:02 Marc