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_map
siempre almacena internamente Foo
objetos (y no, digamos, Foo *
s) como claves, que se destruyen cuando unordered_map
se destruye. Aquí, las unordered_map
teclas internas de los foos 13, 11, 5, 10, 7 y 9.
- Técnicamente, nuestro
unordered_map
realmente almacena std::pair<const Foo, int>
objetos, que a su vez almacenan los Foo
objetos. 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::pair
objeto 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::pair
objeto intermediario unordered_map
introduce tecnicismos sutiles, pero importantes.
Insertar cada una de foo0
, foo1
y foo2
requirió 2 llamadas a uno de Foo
los constructores de copiar / mover y 2 llamadas aFoo
destructor (como describo ahora):
a. Al insertar cada uno de ellos foo0
y foo1
crear un objeto temporal ( foo4
y foo6
, respectivamente) cuyo destructor fue llamado inmediatamente después de que se completó la inserción. Además, los Foo
s internos del unordered_map (que son Foo
s 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ó Foo
al constructor de copia en foo2
(creando foo8
como un miembro interno de pair
). Luego insert()
editamos este par, lo que resultó en unordered_map
llamar nuevamente al constructor de copias (on foo8
) para crear su propia copia interna ( foo9
). Al igual que con foo
s 0 y 1, el resultado final fue dos llamadas de destructor para esta inserción, con la única diferencia de que ese foo8
destructor se llamó solo cuando llegamos al final de, en main()
lugar de ser llamado inmediatamente después de insert()
finalizar.
El empalme foo3
resultó en solo 1 llamada al constructor copiar / mover (creando foo10
internamente en unordered_map
) y solo 1 llamada al Foo
destructor. (Volveré a esto más tarde).
Para foo11
, pasamos directamente el entero 11 a emplace(11, d)
para que unordered_map
llame 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 foo
objeto preexistente para hacer esto. Es importante destacar que solo se Foo
produjo 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 Foo
constructor), esta llamada a insert({12, d})
resultó en dos llamadas al Foo
constructor (creación foo12
y 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 Foo
objeto dentro main()
del alcance (seguido de una copia o movimiento), si se usa, emplace()
entonces cualquier llamada a un Foo
constructor 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 Foo
llamada 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_map
las 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 Foo
es el siguiente: dado que estamos usando emplace()
, el compilador sabe que foo3
(un Foo
objeto no const ) está destinado a ser un argumento para algún Foo
constructor. En este caso, el Foo
constructor 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::pair
constructor (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::set
or std::unordered_multiset
) en lugar destd::unordered_map
.
C. Ahora use un Goo
objeto (solo una copia renombrada de Foo
) en lugar de an int
como 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é Goo
constructores se llaman. (Spoiler: hay un efecto pero no es muy dramático).