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_back
vs. se emplace_back
pierden 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_back
vs. emplace_back
a 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_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 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á hacia adentro, v
mientras emplace_back
que 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_back
llamará a cualquier tipo de constructor, mientras que el más cauteloso push_back
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 a U
partir de a T
, está diciendo que U
puede contener toda la información T
sin pérdida. Es seguro en casi cualquier situación pasar un T
y a nadie le importará si lo haces en su 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
a 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_back
puede llamar a constructores explícitos, pasar un puntero no propietario compila perfectamente. Sin embargo, cuando está v
fuera de alcance, el destructor intentará llamar delete
a ese puntero, que no fue asignado new
porque 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_back
a 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_back
puede llamar a constructores explícitos, pasar un puntero no propietario compila perfectamente. Sin embargo, cuando estáv
fuera de alcance, el destructor intentará llamardelete
a ese puntero, que no fue asignadonew
porque es solo un objeto de pila. Esto lleva a un comportamiento indefinido.explicit
constructor es un constructor al que se leexplicit
aplica la palabra clave . Un constructor "implícito" es cualquier constructor que no tiene esa palabra clave. En el caso delstd::unique_ptr
constructor deT *
, el implementador destd::unique_ptr
escribió 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_back
siempre 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 daraggregate
un 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_back
sería preferible, ya que llama al constructor en el lugar. Y, por lo tanto, no requiere que el tipo sea copiable.emplace
agregar 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_back
sea preferible.emplace_back
es una versión "excelente" de . Es una versión potencialmente peligrosa de la misma. Lee las otras respuestas.push_back
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.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_back
resultados 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_back
ser diferente en este caso?