insertar vs emplace vs operador[] en el mapa c++
dictionary operators (4)
Estoy usando mapas por primera vez y me di cuenta de que hay muchas maneras de insertar un elemento. Puede usar emplace()
, operator[]
o insert()
, además de variantes como usar value_type
o make_pair
. Si bien hay mucha información sobre todos ellos y preguntas sobre casos particulares, aún no puedo entender el panorama general. Entonces, mis dos preguntas son:
¿Cuál es la ventaja de cada uno de ellos sobre los demás?
¿Hubo alguna necesidad de agregar emplazamiento al estándar? ¿Hay algo que no fuera posible antes sin eso?
Además de las oportunidades de optimización y la sintaxis más simple, una distinción importante entre la inserción y el emplazamiento es que este último permite conversiones explícitas . (Esto se encuentra en toda la biblioteca estándar, no solo para mapas).
Aquí hay un ejemplo para demostrar:
#include <vector>
struct foo
{
explicit foo(int);
};
int main()
{
std::vector<foo> v;
v.emplace(v.end(), 10); // Works
//v.insert(v.end(), 10); // Error, not explicit
v.insert(v.end(), foo(10)); // Also works
}
Sin duda, se trata de un detalle muy específico, pero cuando se trata de cadenas de conversiones definidas por el usuario, vale la pena tener esto en cuenta.
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>
struct Foo {
static int foo_counter;
int val;
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;
std::cout << "/numap.insert(std::pair<Foo, int>(foo0, d))/n";
umap.insert(std::pair<Foo, int>(foo0, d));
//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)));
//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 objetosFoo
(y no, por ejemplo,Foo *
s) como claves, que se destruyen cuando se destruye elunordered_map
. Aquí, las claves internas de unordered_map eran foos 13, 11, 5, 10, 7 y 9.- Por lo tanto, técnicamente, nuestro
unordered_map
realidad almacena objetosstd::pair<const Foo, int>
, que a su vez almacenan los objetosFoo
. Pero para comprender la "idea general" de cómo se diferenciaemplace()
deinsert()
(vea el recuadro resaltado a continuación), está bien imaginar temporalmente que este objetostd::pair
es completamente pasivo. Una vez que comprenda esta "idea general", es importante hacer una copia de seguridad y comprender cómo el uso de este objeto intermediariostd::pair
medianteunordered_map
introduce tecnicismos sutiles pero importantes.
- Por lo tanto, técnicamente, nuestro
Insertar cada uno de
foo0
,foo1
yfoo2
requirió 2 llamadas a uno de los constructores de copiar / mover deFoo
y 2 llamadas al destructor deFoo
(como ahora lo describo):a. Al insertar cada uno de
foo0
yfoo1
creó un objeto temporal (foo4
yfoo6
, respectivamente) cuyo destructor fue llamado inmediatamente después de que se completara la inserción. Además, losFoo
internos de unordered_map (que son foos 5 y 7) también tenían sus destructores llamados cuando se destruyó el mapa desordenado.segundo. Para insertar
foo2
, primero creamos un par no temporal (foo8
), que llamaba al constructor de copia deFoo
, y luego lofoo8
, lo que dio como resultado que unordered_map llamara al constructor de copia nuevamente para crear una copia interna (foo9
). Al igual que con los foos 0 y 1, el resultado final fueron dos llamadas al destructor para esta inserción con la única diferencia de que elfoo8
defoo8
solo sefoo8
cuando llegamos al final demain()
.foo3
resultó en solo 1 copia / llamada de constructor de movimiento (creandofoo10
internamente) y solo 1 llamada al destructor deFoo
. (Volveré a esto más tarde).Para
foo11
, pasamos directamente el número entero 11 para queunordered_map
llamara al constructorFoo(int)
. A diferencia de (3), ni siquiera necesitábamos algún objeto Foo preexistente para hacer esto.
Esto muestra cuál es la diferencia principal de "panorama" entre insert()
y emplace()
:
Mientras que el uso de
insert()
casi siempre requiere la construcción o existencia de algún objetoFoo
en el alcance demain()
(seguido de una copia o movimiento), si usaemplace()
entonces cualquier llamada a un constructor se realiza completamente internamente en elunordered_map
(es decir, dentro del alcance de la definición del métodoemplace()
). El argumento (s) para la clave que pasa aemplace()
se reenvía directamente a una llamada de constructorFoo
dentro deunordered_map
(detalles adicionales opcionales: donde este objeto recién construido se incorpora inmediatamente en una de las variables miembro de unordered_map para que no destructor seemplace()
cuando la ejecución deja aemplace()
y no se llaman constructores de movimiento o copia).
Nota: La razón de la casi en casi siempre se explica en I) a continuación.
- continúa: La razón por la cual llamar a
umap.emplace(foo3, d)
llamado constructor de copias noumap.emplace(foo3, d)
Foo
es el siguiente: dado que estamos usandoemplace()
, el compilador sabe quefoo3
(un objetoFoo
no const) es pretende ser un argumento para algún constructor deFoo
. En este caso, el constructor deFoo
más apropiado es la construcción de copia sin conformismoFoo(Foo& f2)
. Esta es la razónumap.emplace(foo3, d)
cualumap.emplace(foo3, d)
llamó a un constructor de copia mientras queumap.emplace(11, d)
no lo hizo.
Epílogo:
I. Tenga en cuenta que una sobrecarga de insert()
es en realidad equivalente a emplace()
. Como se describe en esta página cppreference.com , la template<class P> std::pair<iterator, bool> insert(P&& value)
sobrecarga template<class P> std::pair<iterator, bool> insert(P&& value)
(que es sobrecarga (2) de insert()
en esta página cppreference.com) es equivalente para emplace(std::forward<P>(value))
.
II. A dónde ir desde aquí?
a. Juega con el código fuente anterior y la documentación de estudio para insert()
(por ejemplo, aquí ) y emplace()
(por ejemplo, here ) que se encuentra en línea. Si está utilizando un IDE como eclipse o NetBeans, puede hacer que su IDE le diga con facilidad qué sobrecarga de insert()
o emplace()
se llama (en eclipse, simplemente mantenga el cursor del mouse sobre la llamada de función para un segundo). Aquí hay un código más 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 to 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 cals
// 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á que qué sobrecarga del constructor std::pair
(ver reference ) termina siendo usado por unordered_map
puede tener un efecto importante en la cantidad de objetos que se copian, mueven, crean y / o destruyen y cuando esto ocurre.
segundo. Vea lo que sucede cuando utiliza alguna otra clase de contenedor (por ejemplo, std::set
o std::unordered_multiset
) en lugar de std::unordered_map
.
do. Ahora usa un objeto Goo
(solo una copia renombrada de Foo
) en lugar de un int
como tipo de rango en un unordered_map
(es decir, usa unordered_map<Foo, Goo>
lugar de unordered_map<Foo, int>
) y ve cuántos y qué constructores de Goo
son llamados. (Spoiler: hay un efecto pero no es muy dramático.)
Emplace: aprovecha la referencia rvalue para usar los objetos reales que ya ha creado. Esto significa que no se llama a ningún constructor de copiar o mover, ¡es bueno para objetos GRANDES! O (log (N)) tiempo.
Insertar: tiene sobrecargas para referencia de valor l estándar y referencia de valor r, así como también iteradores para listas de elementos para insertar y "sugerencias" sobre la posición a la que pertenece un elemento. El uso de un iterador "sugerencia" puede hacer que la inserción de tiempo disminuya al tiempo de contant, de lo contrario es el tiempo O (log (N)).
Operador []: comprueba si el objeto existe y, si lo hace, modifica la referencia a este objeto; de lo contrario, utiliza la clave y el valor proporcionados para llamar a make_pair en los dos objetos y luego realiza el mismo trabajo que la función de inserción. Este es el tiempo O (log (N)).
make_pair: Hace poco más que hacer un par.
No hubo "necesidad" de agregar emplazamiento al estándar. En c ++ 11 creo que se agregó el tipo de referencia &&. Esto eliminó la necesidad de semántica de movimiento y permitió la optimización de algún tipo específico de gestión de memoria. En particular, la referencia rvalue. El operador de inserción sobrecargado (value_type &&) no aprovecha la semántica in_place y, por lo tanto, es mucho menos eficiente. Si bien brinda la capacidad de tratar con las referencias de valor real, ignora su propósito clave, que es la construcción de objetos.
En el caso particular de un mapa, las opciones anteriores eran solo dos: operator[]
e insert
(diferentes sabores de insert
). Entonces comenzaré a explicarlos.
El operator[]
es un operator[]
buscar o agregar . Tratará de encontrar un elemento con la clave dada dentro del mapa, y si existe, devolverá una referencia al valor almacenado. Si no lo hace, creará un nuevo elemento insertado en su lugar con la inicialización predeterminada y le devolverá una referencia.
La función de insert
(en el elemento único sabor) toma un value_type
( std::pair<const Key,Value>
), usa la clave ( first
miembro) e intenta insertarlo. Debido a que std::map
no permite duplicados si hay un elemento existente, no insertará nada.
La primera diferencia entre los dos es que el operator[]
necesita poder construir un valor inicializado predeterminado, y por lo tanto no se puede utilizar para los tipos de valores que no pueden inicializarse por defecto. La segunda diferencia entre los dos es lo que sucede cuando ya hay un elemento con la clave dada. La función de insert
no modificará el estado del mapa, sino que devolverá un iterador al elemento (y un false
que indica que no se insertó).
// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10; // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10
En el caso de insert
el argumento es un objeto de value_type
, que se puede crear de diferentes maneras. Puedes construirlo directamente con el tipo apropiado o pasar cualquier objeto desde el que se pueda construir value_type
, que es donde std::make_pair
entra en juego, ya que permite la creación simple de objetos std::pair
, aunque probablemente no sea Lo que quieras...
El efecto neto de las siguientes llamadas es similar :
K t; V u;
std::map<K,V> m; // std::map<K,V>::value_type is std::pair<const K,V>
m.insert( std::pair<const K,V>(t,u) ); // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) ); // 3
Pero en realidad no son lo mismo ... [1] y [2] son en realidad equivalentes. En ambos casos, el código crea un objeto temporal del mismo tipo ( std::pair<const K,V>
) y lo pasa a la función de insert
. La función de insert
creará el nodo apropiado en el árbol de búsqueda binaria y luego copiará la parte value_type
del argumento al nodo. La ventaja de utilizar value_type
es que, bueno, value_type
siempre coincide con value_type
, ¡no se puede confundir el tipo de los argumentos std::pair
!
La diferencia está en [3]. La función std::make_pair
es una función de plantilla que creará un std::pair
. La firma es:
template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );
Intencionalmente no proporcioné los argumentos de la plantilla a std::make_pair
, ya que ese es el uso común. Y la implicación es que los argumentos de la plantilla se deducen de la llamada, en este caso ser T==K,U==V
, por lo que la llamada a std::make_pair
devolverá un std::pair<K,V>
( nota la const
faltante). La firma requiere value_type
que está cerca pero no es lo mismo que el valor devuelto de la llamada a std::make_pair
. Debido a que está lo suficientemente cerca, creará un tipo temporal del tipo correcto y lo inicializará. Eso, a su vez, se copiará en el nodo, creando un total de dos copias.
Esto se puede solucionar proporcionando los argumentos de la plantilla:
m.insert( std::make_pair<const K,V>(t,u) ); // 4
Pero eso sigue siendo propenso a errores de la misma manera que escribiendo explícitamente el tipo en el caso [1].
Hasta este punto, tenemos diferentes formas de llamar a insert
que requieren la creación de value_type
externamente y la copia de ese objeto en el contenedor. Alternativamente, puede usar el operator[]
si el tipo es predecible, construible y asignable (enfocando intencionalmente solo en m[k]=v
), y requiere la inicialización predeterminada de un objeto y la copia del valor en ese objeto.
En C ++ 11, con plantillas variadas y un reenvío perfecto, hay una nueva forma de agregar elementos a un contenedor por medio de la colocación (creación en el lugar). Las funciones emplace
en los diferentes contenedores hacen básicamente lo mismo: en lugar de obtener una fuente desde la cual copiar en el contenedor, la función toma los parámetros que se reenviarán al constructor del objeto almacenado en el contenedor.
m.emplace(t,u); // 5
En [5], std::pair<const K, V>
no se crea y se pasa a la emplace
, sino que las referencias al objeto t
y u
se pasan a la emplace
que las reenvía al constructor del subobjeto value_type
dentro de los datos estructura. En este caso, no se realizan copias de std::pair<const K,V>
, lo cual es una ventaja sobre las alternativas de C ++ 03. Como en el caso de insert
, no anulará el valor en el mapa.
Una pregunta interesante sobre la que no había pensado es cómo se puede implementar en realidad un mapa, y ese no es un problema simple en el caso general.