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

232

Los vectores C ++ 11 tienen la nueva función emplace_back. A diferencia push_back, que se basa en las optimizaciones del compilador para evitar copias, emplace_backutiliza el reenvío perfecto para enviar los argumentos directamente al constructor para crear un objeto en el lugar. Me parece que emplace_backhace todo lo que push_backpuede hacer, pero algunas veces lo hará mejor (pero nunca peor).

¿Qué razón tengo que usar push_back?

David Stone
fuente

Respuestas:

162

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_backvs. se emplace_backpierden 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_backvs. emplace_backa 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_backo 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_backconstruirá un objeto temporal, que luego se moverá hacia adentro, vmientras emplace_backque 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_backllamará a cualquier tipo de constructor, mientras que el más cauteloso push_backllamará 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 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 en su Ulugar. Un buen ejemplo de un constructor implícito es la conversión de std::uint32_ta std::uint64_t. Un mal ejemplo de una 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 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_backpuede llamar a constructores explícitos, pasar un puntero no propietario compila perfectamente. Sin embargo, cuando está vfuera de alcance, el destructor intentará llamar deletea ese puntero, que no fue asignado newporque 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_backa 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.

David Stone
fuente
Sería útil si pudiera explicar qué hace exactamente el emplazamiento en su ejemplo y por qué está mal.
eddi
44
@eddi: agregué una sección que explica esto: std::unique_ptr<T>tiene un constructor explícito de T *. Debido a que emplace_backpuede llamar a constructores explícitos, pasar un puntero no propietario compila perfectamente. Sin embargo, cuando está vfuera de alcance, el destructor intentará llamar deletea ese puntero, que no fue asignado newporque es solo un objeto de pila. Esto lleva a un comportamiento indefinido.
David Stone
Gracias por publicar esto. No lo sabía cuando escribí mi respuesta, pero ahora desearía haberlo escrito yo mismo cuando lo aprendí después :) Realmente quiero abofetear a las personas que cambian a nuevas características solo para hacer lo más moderno que puedan encontrar . Chicos, las personas también usaban C ++ antes de C ++ 11, y no todo era problemático. Si no sabe por qué está usando una función, no la use . Me alegro de que hayas publicado esto y espero que reciba más votos a favor, así que va por encima del mío. +1
user541686
1
@CaptainJacksparrow: Parece que digo implícito y explícito a qué me refiero. ¿Qué parte has confundido?
David Stone
44
@CaptainJacksparrow: un explicitconstructor es un constructor al que se le explicitaplica la palabra clave . Un constructor "implícito" es cualquier constructor que no tiene esa palabra clave. En el caso del std::unique_ptrconstructor de T *, el implementador de std::unique_ptrescribió ese constructor, pero el problema aquí es que el usuario de ese tipo llamó emplace_back, que llamó a ese constructor explícito. Si lo hubiera sido push_back, en lugar de llamar a ese constructor, habría dependido de una conversión implícita, que solo puede llamar a constructores implícitos.
David Stone el
117

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
fuente
58
Tenga en cuenta que esto solo se aplica a la inicialización agregada y la inicialización de la lista de inicializadores. Si usaría la {}sintaxis para llamar a un constructor real, puede simplemente eliminar los {}'s y usar emplace_back.
Nicol Bolas
Pregunta tonta: ¿entonces emplace_back no se puede usar para vectores de estructuras? ¿O simplemente no para este estilo usando el literal {42,121}?
Phil H
1
@LucDanton: Como dije, solo se aplica a la inicialización de lista agregada e inicializadora. Puede usar la {}sintaxis para llamar a constructores reales. Podría dar aggregateun constructor que tome 2 enteros, y este constructor se llamaría al usar la {}sintaxis. El punto es que si está intentando llamar a un constructor, emplace_backsería preferible, ya que llama al constructor en el lugar. Y, por lo tanto, no requiere que el tipo sea copiable.
Nicol Bolas
11
Esto fue visto como un defecto en el estándar y ha sido resuelto. Ver cplusplus.github.io/LWG/lwg-active.html#2089
David Stone el
3
@DavidStone Si se hubiera resuelto, todavía no estaría en la lista "activa" ... ¿no? Parece seguir siendo un tema pendiente. La última actualización, titulada " [23-08-2018 Procesamiento de problemas de Batavia] ", dice que " P0960 (actualmente en vuelo) debería resolver esto " . Y todavía no puedo compilar código que intente emplaceagregar agregados sin escribir explícitamente un constructor repetitivo . Tampoco está claro en este momento si será tratado como un defecto y, por lo tanto, elegible para el backport, o si los usuarios de C ++ <20 seguirán siendo SoL.
underscore_d
80

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

usuario541686
fuente
21
Esa parece ser la maldición de C ++. Con cada nueva versión, obtenemos toneladas de funciones interesantes, pero muchas compañías están estancadas usando alguna versión antigua por razones de compatibilidad o desalentando (si no rechazando) el uso de ciertas funciones.
Dan Albert
66
@Mehrdad: ¿Por qué conformarse con lo suficiente cuando puedes tener grandes? Seguro que no quisiera programar en blub , incluso si fuera suficiente. No digo que ese sea el caso para este ejemplo en particular, pero como alguien que pasa la mayor parte de su tiempo programando en C89 por razones de compatibilidad, definitivamente es un problema real.
Dan Albert
3
No creo que esta sea realmente una respuesta a la pregunta. Para mí, está pidiendo casos de uso donde push_backsea ​​preferible.
Sr. Boy
3
@ Mr.Boy: es preferible cuando desea ser compatible con los compiladores anteriores a C ++ 11. ¿No estaba claro en mi respuesta?
user541686
66
Esto ha recibido mucha más atención de la que esperaba, así que para todos ustedes que lean esto: noemplace_back es una versión "excelente" de . Es una versión potencialmente peligrosa de la misma. Lee las otras respuestas. push_back
user541686
68

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

Para acomodar errores conocidos del compilador, prefiera usar 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_backresultados en un cambio de tamaño en ese punto se v[0]vuelve inválido.

Se usaría una implementación funcional del código anterior en push_back()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 entradas 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()provocar un bloqueo duro.

Bagazo
fuente
Espere. Esto parece ser un error. ¿Cómo puede push_backser diferente en este caso?
balki
12
La llamada a emplace_back () utiliza el reenvío perfecto para realizar la construcción en el lugar y, como tal, v [0] no se evalúa hasta después de que el vector se haya redimensionado (en cuyo punto v [0] no es válido). push_back construye el nuevo elemento y copia / mueve el elemento según sea necesario y v [0] se evalúa antes de cualquier reasignación.
Marc
1
@Marc: el estándar garantiza que emplace_back funciona incluso para elementos dentro del rango.
David Stone
1
@DavidStone: ¿Podría proporcionar una referencia de en qué parte del estándar está garantizado este comportamiento? De cualquier manera, Visual Studio 2012 y 2015 exhiben un comportamiento incorrecto.
Marc
44
@cameino: emplace_back existe para retrasar la evaluación de su parámetro para reducir la copia innecesaria. El comportamiento es indefinido o un error del compilador (pendiente de análisis del estándar). Recientemente ejecuté la misma prueba con Visual Studio 2015 y obtuve 123,3 en la versión x64, 123,40 en la versión Win32 y 123, -572662307 en Debug x64 y Debug Win32.
Marc