¿Por qué iba a usar 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 optimizaciones del compilador para evitar copias, emplace_back utiliza perfect forwarding para enviar los argumentos directamente al constructor para crear un objeto en el lugar. Me parece que emplace_back hace todo lo que push_back puede hacer, pero algunas veces lo hará mejor (pero nunca peor).

¿Qué razón tengo para usar push_back?

 176
Author: ildjarn, 2012-06-05

4 answers

push_back siempre permite el uso de la inicialización uniforme, que 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á.

 103
Author: Luc Danton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-05 21:39:36

He pensado bastante sobre esta cuestión durante los últimos cuatro años. He llegado a la conclusión de que la mayoría de las explicaciones acerca de push_back vs. emplace_back pierden el cuadro completo.

El año pasado, di una presentación en C++Now sobre Deducción de tipos en C++14. Empiezo a hablar de 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 primaria real tiene que ver con implícita vs. explícita constructor. Considere el caso donde tenemos un solo argumento que queremos pasar a 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 tenga esto en sus manos, no hay diferencia entre estas dos sentencias 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 enviará el argumento y lo construirá directamente en su lugar sin copias ni movimientos. Esto puede ser verdad basada en el código como está escrito en las bibliotecas estándar, pero hace la suposición errónea de que el trabajo del compilador de optimización es generar el código que 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 la plataforma y no se preocupara por la capacidad de mantenimiento, solo por el rendimiento.

La diferencia real entre estas dos sentencias es que el más poderoso emplace_back llamará a cualquier tipo de constructor que haya, 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 puedes construir implícitamente un U a partir de un T, estás diciendo que U puede contener toda la información en T sin pérdida. Es seguro en casi cualquier situación pasar un T y a nadie le importará si lo haces un U en su 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 usar funciones potentes porque cuanto más potente sea la función, más fácil será hacer algo incorrecto o inesperado accidentalmente. Si tiene la intención de llamar a constructores explícitos, entonces necesita la potencia de emplace_back. Si desea llamar solo 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 *. Porque emplace_back puede llamar a constructores explícitos, pasando un puntero no propietario compila muy bien. Sin embargo, cuando v sale del ámbito, el destructor intentará llamar a delete en ese puntero, que no fue asignado por new porque es solo un objeto de pila. Esto conduce a un comportamiento indefinido.

Esto no es solo un código inventado. Este fue un verdadero error de producción que encontré. El código era std::vector<T *>, pero poseía el contenido. Como parte de la migración a C++11, cambié correctamente T * a std::unique_ptr<T> para indicar que el vector poseía su memoria. Sin embargo, estaba basando estos cambios en mi entendimiento en 2012, durante el cual pensé "emplace_back hace todo lo que push_back puede hacer y más, así que ¿por qué iba a usar push_back?", así que también cambié el push_back a emplace_back.

Si en su lugar hubiera dejado el código como el uso de la más segura push_back, habría captado al instante este error de larga data y habría sido visto como un éxito de la actualización a C++11. En su lugar, enmascaré el insecto y no encuéntralo hasta meses después.

 86
Author: David Stone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-31 22:21:29

Compatibilidad hacia atrás con compiladores pre-C++11.

 78
Author: Mehrdad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-05 02:03:21

Algunas implementaciones de bibliotecas 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 usarstd::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 esperados 123 y 123. Esto se debe al hecho de que la 2da llamada a emplace_back resulta en un cambio de tamaño en el que v[0] se convierte en inválido.

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

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

Nota: El uso de un vector de ints 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 duro.

 63
Author: Marc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-11 18:08:01