El siguiente código puede ayudarlo a comprender la "idea general" de cómo insert()difiere de emplace():
#include <iostream>
#include <unordered_map>
#include <utility>
//Foo simply outputs what constructor is called with what value.
struct Foo {
static int foo_counter; //Track how many Foo objects have been created.
int val; //This Foo object was the val-th Foo object to be created.
Foo() { val = foo_counter++;
std::cout << "Foo() with val: " << val << '\n';
}
Foo(int value) : val(value) { foo_counter++;
std::cout << "Foo(int) with val: " << val << '\n';
}
Foo(Foo& f2) { val = foo_counter++;
std::cout << "Foo(Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(const Foo& f2) { val = foo_counter++;
std::cout << "Foo(const Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(Foo&& f2) { val = foo_counter++;
std::cout << "Foo(Foo&&) moving: " << f2.val
<< " \tand changing it to:\t" << val << '\n';
}
~Foo() { std::cout << "~Foo() destroying: " << val << '\n'; }
Foo& operator=(const Foo& rhs) {
std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
<< " \tcalled with lhs.val = \t" << val
<< " \tChanging lhs.val to: \t" << rhs.val << '\n';
val = rhs.val;
return *this;
}
bool operator==(const Foo &rhs) const { return val == rhs.val; }
bool operator<(const Foo &rhs) const { return val < rhs.val; }
};
int Foo::foo_counter = 0;
//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
template<> struct hash<Foo> {
std::size_t operator()(const Foo &f) const {
return std::hash<int>{}(f.val);
}
};
}
int main()
{
std::unordered_map<Foo, int> umap;
Foo foo0, foo1, foo2, foo3;
int d;
//Print the statement to be executed and then execute it.
std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
umap.insert(std::pair<Foo, int>(foo0, d));
//Side note: equiv. to: umap.insert(std::make_pair(foo0, d));
std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
//Side note: equiv. to: umap.insert(std::make_pair(foo1, d));
std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
std::pair<Foo, int> pair(foo2, d);
std::cout << "\numap.insert(pair)\n";
umap.insert(pair);
std::cout << "\numap.emplace(foo3, d)\n";
umap.emplace(foo3, d);
std::cout << "\numap.emplace(11, d)\n";
umap.emplace(11, d);
std::cout << "\numap.insert({12, d})\n";
umap.insert({12, d});
std::cout.flush();
}
El resultado que obtuve fue:
Foo() with val: 0
Foo() with val: 1
Foo() with val: 2
Foo() with val: 3
umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val: 4 created from: 0
Foo(Foo&&) moving: 4 and changing it to: 5
~Foo() destroying: 4
umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val: 6 created from: 1
Foo(Foo&&) moving: 6 and changing it to: 7
~Foo() destroying: 6
std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val: 8 created from: 2
umap.insert(pair)
Foo(const Foo &) with val: 9 created from: 8
umap.emplace(foo3, d)
Foo(Foo &) with val: 10 created from: 3
umap.emplace(11, d)
Foo(int) with val: 11
umap.insert({12, d})
Foo(int) with val: 12
Foo(const Foo &) with val: 13 created from: 12
~Foo() destroying: 12
~Foo() destroying: 8
~Foo() destroying: 3
~Foo() destroying: 2
~Foo() destroying: 1
~Foo() destroying: 0
~Foo() destroying: 13
~Foo() destroying: 11
~Foo() destroying: 5
~Foo() destroying: 10
~Foo() destroying: 7
~Foo() destroying: 9
Darse cuenta de:
Un unordered_mapsiempre almacena internamente Fooobjetos (y no, digamos, Foo *s) como claves, que se destruyen cuando unordered_mapse destruye. Aquí, las unordered_mapteclas internas de los foos 13, 11, 5, 10, 7 y 9.
- Técnicamente, nuestro
unordered_maprealmente almacena std::pair<const Foo, int>objetos, que a su vez almacenan los Fooobjetos. Pero para comprender la "idea general" de cómo emplace()difiere de insert()(ver el cuadro resaltado a continuación), está bien imaginar temporalmente este std::pairobjeto como completamente pasivo. Una vez que comprenda esta "idea general", es importante hacer una copia de seguridad y comprender cómo el uso de este std::pairobjeto intermediario unordered_mapintroduce tecnicismos sutiles, pero importantes.
Insertar cada una de foo0, foo1y foo2requirió 2 llamadas a uno de Foolos constructores de copiar / mover y 2 llamadas aFoo destructor (como describo ahora):
a. Al insertar cada uno de ellos foo0y foo1crear un objeto temporal ( foo4y foo6, respectivamente) cuyo destructor fue llamado inmediatamente después de que se completó la inserción. Además, los Foos internos del unordered_map (que son Foos 5 y 7) también tenían sus destructores llamados cuando el unordered_map fue destruido.
si. Para insertar foo2, en su lugar, primero creamos explícitamente un objeto de par no temporal (llamado pair), que llamó Fooal constructor de copia en foo2(creando foo8como un miembro interno de pair). Luego insert()editamos este par, lo que resultó en unordered_mapllamar nuevamente al constructor de copias (on foo8) para crear su propia copia interna ( foo9). Al igual que con foos 0 y 1, el resultado final fue dos llamadas de destructor para esta inserción, con la única diferencia de que ese foo8destructor se llamó solo cuando llegamos al final de, en main()lugar de ser llamado inmediatamente después de insert()finalizar.
El empalme foo3resultó en solo 1 llamada al constructor copiar / mover (creando foo10internamente en unordered_map) y solo 1 llamada al Foodestructor. (Volveré a esto más tarde).
Para foo11, pasamos directamente el entero 11 a emplace(11, d)para que unordered_mapllame al Foo(int)constructor mientras la ejecución está dentro de su emplace()método. A diferencia de (2) y (3), ni siquiera necesitábamos algún fooobjeto preexistente para hacer esto. Es importante destacar que solo se Fooprodujo 1 llamada a un constructor (que creó foo11).
Luego pasamos directamente el entero 12 a insert({12, d}). A diferencia de con emplace(11, d)(cuya recuperación resultó en solo 1 llamada a un Fooconstructor), esta llamada a insert({12, d})resultó en dos llamadas al Fooconstructor (creación foo12y foo13).
Esto muestra cuál es la principal diferencia de "panorama general" entre insert()y emplace()es:
Mientras que el uso insert() casi siempre requiere la construcción o existencia de algún Fooobjeto dentro main()del alcance (seguido de una copia o movimiento), si se usa, emplace()entonces cualquier llamada a un Fooconstructor se realiza completamente internamente en unordered_map(es decir, dentro del alcance de la emplace()definición del método). Los argumentos de la clave a la que pasa emplace()se reenvían directamente a una Foollamada de constructor dentro unordered_map::emplace()de la definición (detalles adicionales opcionales: donde este objeto recién construido se incorpora de inmediato a una de unordered_maplas variables miembro para que no se llame a ningún destructor cuando la ejecución se va emplace()y no se llama ningún constructor de movimiento o copia).
Nota: La razón para el " casi " en " casi siempre " arriba se explica en I) a continuación.
- continuación: La razón por la cual llamar al constructor de copia no const
umap.emplace(foo3, d)llamada Fooes el siguiente: dado que estamos usando emplace(), el compilador sabe que foo3(un Fooobjeto no const ) está destinado a ser un argumento para algún Fooconstructor. En este caso, el Fooconstructor más adecuado es el constructor de copia no constante Foo(Foo& f2). Es por eso que umap.emplace(foo3, d)llamó a un constructor de copia mientras umap.emplace(11, d)que no lo hizo.
Epílogo:
I. Tenga en cuenta que una sobrecarga de en insert()realidad es equivalente a emplace() . Como se describe en esta página de cppreference.com , la sobrecarga template<class P> std::pair<iterator, bool> insert(P&& value)(que es la sobrecarga (2) de insert()en esta página de cppreference.com) es equivalente a emplace(std::forward<P>(value)).
II A dónde ir desde aquí?
a. Juegue con el código fuente anterior y la documentación de estudio para insert()(por ejemplo, aquí ) y emplace()(por ejemplo, aquí ) que se encuentra en línea. Si está utilizando un IDE como eclipse o NetBeans, puede obtener fácilmente su IDE para indicarle qué sobrecarga insert()o qué emplace()se está llamando (en eclipse, solo mantenga el cursor del mouse fijo sobre la llamada de función por un segundo). Aquí hay más código para probar:
std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!
std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&).
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy
// constructors, despite the below call's only difference from the call above
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});
//Pay close attention to the subtle difference in the effects of the next
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where "
<< "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});
std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
<< "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});
//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));
Pronto verás qué sobrecarga del std::pairconstructor (ver referencia ) termina siendo utilizada porunordered_map puede tener un efecto importante en la cantidad de objetos que se copian, mueven, crean y / o destruyen, así como cuándo ocurre todo esto.
si. Vea lo que sucede cuando usa alguna otra clase de contenedor (por ejemplo, std::setor std::unordered_multiset) en lugar destd::unordered_map .
C. Ahora use un Gooobjeto (solo una copia renombrada de Foo) en lugar de an intcomo el tipo de rango en un unordered_map(es decir, use en unordered_map<Foo, Goo>lugar de unordered_map<Foo, int>) y vea cuántos y a qué Gooconstructores se llaman. (Spoiler: hay un efecto pero no es muy dramático).