¿Por qué alguna vez usaría 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 las optimizaciones del comstackdor para evitar copias, emplace_back utiliza reenvío perfecto para enviar los argumentos directamente al constructor para crear un objeto in situ. Me parece que emplace_back puede hacer todo lo que push_back puede hacer, pero algunas veces lo hará mejor (pero nunca peor).

¿Qué razón tengo para usar push_back ?

push_back siempre permite el uso de una inicialización uniforme, que me gusta mucho. Por ejemplo:

 struct aggregate { int foo; int bar; }; std::vector v; v.push_back({ 42, 121 }); 

Por otro lado, v.emplace_back({ 42, 121 }); no trabajará.

He pensado bastante sobre esta cuestión en 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, di una presentación en C ++ Now sobre Type Deduction en C ++ 14 . Comienzo a hablar de push_back vs. emplace_back a las 13:49, pero hay información útil que proporciona cierta evidencia de respaldo antes de eso.

La diferencia principal real tiene que ver con los constructores implícitos frente a los explícitos. Considere el caso en el que tenemos un único argumento que queremos pasar a push_back o emplace_back .

 std::vector v; v.push_back(x); v.emplace_back(x); 

Después de que su comstackdor 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á a v mientras que emplace_back reenviará el argumento y lo construirá directamente en su lugar sin copias ni movimientos. Esto puede ser cierto basado en el código tal como está escrito en las bibliotecas estándar, pero supone erróneamente que el trabajo del comstackdor de optimización es generar el código que escribió. La tarea del optimizador del comstackdor es en realidad generar el código que habría escrito si fuera un experto en optimizaciones específicas de la plataforma y no se preocupara por la facilidad de mantenimiento, solo por el rendimiento.

La diferencia real entre estas dos afirmaciones es que el emplace_back más poderoso emplace_back a cualquier tipo de constructor, mientras que el push_back más cauteloso solo llamará a los constructores que estén implícitos. Se supone que los constructores implícitos son seguros. Si puedes construir implícitamente una U partir de una T , estás diciendo que U puede contener toda la información en T sin pérdida. En casi cualquier situación, es seguro pasar una T y a nadie le importará si la convierte en una 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 para std::uint8_t .

Queremos ser cautos en nuestra progtwigción. No queremos utilizar funciones potentes porque cuanto más potente sea la función, más fácil será accidentalmente hacer 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 solo a constructores implícitos, push_back con la seguridad de push_back .

Un ejemplo

 std::vector> v; T a; v.emplace_back(std::addressof(a)); // compiles v.push_back(std::addressof(a)); // fails to compile 

std::unique_ptr tiene un constructor explícito de T * . Como emplace_back puede llamar a constructores explícitos, al pasar un puntero no propietario se comstack muy bien. Sin embargo, cuando v se sale del scope, el destructor intentará llamar a delete en ese puntero, que no fue asignado por new porque es solo un objeto de stack. Esto conduce a un comportamiento indefinido.

Esto no es solo un código inventado. Este fue un error de producción real que encontré. El código era std::vector , pero era el propietario de los contenidos. Como parte de la migración a C ++ 11, cambié correctamente T * a std::unique_ptr para indicar que el vector poseía su memoria. Sin embargo, estaba basando estos cambios en mi comprensión en 2012, durante los cuales pensé que “emplace_back puede hacer todo push_back y más, entonces ¿por qué alguna vez usar push_back?”, Así que también cambié push_back a emplace_back .

Si, en cambio, hubiera dejado el código como usar push_back más push_back , habría captado al instante 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.

Compatibilidad con versiones anteriores con comstackdores anteriores a C ++ 11.

Algunas implementaciones de bibliotecas de emplace_back no se comportan como se especifica en el estándar de C ++, incluida la versión que se envía con Visual Studio 2012, 2013 y 2015.

Para acomodar los errores conocidos del comstackdor, prefiera usar std::vector::push_back() si los parámetros hacen referencia a los iteradores u otros objetos que serán inválidos después de la llamada.

 std::vector v; v.emplace_back(123); v.emplace_back(v[0]); // Produces incorrect results in some compilers 

En un comstackdor, v contiene los valores 123 y 21 en lugar de los 123 y 123 esperados. Esto se debe al hecho de que la 2da llamada a emplace_back da emplace_back resultado un cambio de tamaño en cuyo punto v[0] emplace_back ser válido.

Una implementación operativa del código anterior usaría push_back() lugar de emplace_back() siguiente manera:

 std::vector v; v.emplace_back(123); v.push_back(v[0]); 

Nota: El uso de un vector de ints es con fines de demostración. Descubrí este problema con una clase mucho más compleja que incluía variables miembro dinámicamente asignadas y la llamada a emplace_back() dio como resultado un locking.