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.
std::unique_ptr<T>tiene un constructor explícito deT *. Debido a queemplace_backpuede llamar a constructores explícitos, pasar un puntero no propietario compila perfectamente. Sin embargo, cuando estávfuera de alcance, el destructor intentará llamardeletea ese puntero, que no fue asignadonewporque es solo un objeto de pila. Esto lleva a un comportamiento indefinido.explicitconstructor es un constructor al que se leexplicitaplica la palabra clave . Un constructor "implícito" es cualquier constructor que no tiene esa palabra clave. En el caso delstd::unique_ptrconstructor deT *, el implementador destd::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 sidopush_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.push_backsiempre permite el uso de inicialización uniforme, lo cual me gusta mucho. Por ejemplo:Por otro lado,
v.emplace_back({ 42, 121 });no funcionará.fuente
{}sintaxis para llamar a un constructor real, puede simplemente eliminar los{}'s y usaremplace_back.{}sintaxis para llamar a constructores reales. Podría daraggregateun 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.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.Compatibilidad con versiones anteriores de compiladores anteriores a C ++ 11.
fuente
push_backsea preferible.emplace_backes una versión "excelente" de . Es una versión potencialmente peligrosa de la misma. Lee las otras respuestas.push_backAlgunas 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.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 sev[0]vuelve inválido.Se usaría una implementación funcional del código anterior en
push_back()lugar deemplace_back()lo siguiente: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.fuente
push_backser diferente en este caso?