Estoy usando mapas por primera vez y me di cuenta de que hay muchas formas de insertar un elemento. Puede usar emplace()
, operator[]
o insert()
, más variantes como usar value_type
o make_pair
. Si bien hay mucha información sobre todos ellos y preguntas sobre casos particulares, todavía 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 un lugar al estándar? ¿Hay algo que antes no fuera posible sin él?
operator[]
se basa entry_emplace
. Vale la pena mencionarinsert_or_assign
también.Respuestas:
En el caso particular de un mapa, las opciones anteriores eran solo dos:
operator[]
yinsert
(diferentes sabores deinsert
). Entonces comenzaré a explicar eso.El
operator[]
es un operador de buscar o agregar . Intentará 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 una inicialización predeterminada y le devolverá una referencia.La
insert
función (en el sabor del elemento único) toma unvalue_type
(std::pair<const Key,Value>
), usa la clave (first
miembro) e intenta insertarlo. Debido astd::map
que no permite duplicados si hay un elemento existente, no insertará nada.La primera diferencia entre los dos es que
operator[]
tiene que ser capaz de construir un defecto inicializado valor , y es así inutilizable para los tipos de valor que no puede ser predeterminado inicializado. La segunda diferencia entre los dos es lo que sucede cuando ya hay un elemento con la clave dada. Lainsert
función no modificará el estado del mapa, sino que devolverá un iterador al elemento (y unafalse
indicación de que no se insertó).En el caso del
insert
argumento es un objeto devalue_type
, que se puede crear de diferentes maneras. Puede construirlo directamente con el tipo apropiado o pasar cualquier objeto a partir del cualvalue_type
se pueda construir, que es dondestd::make_pair
entra en juego, ya que permite la creación simple destd::pair
objetos, aunque probablemente no sea lo que desea ...El efecto neto de las siguientes llamadas es similar :
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 lainsert
función. Lainsert
función creará el nodo apropiado en el árbol de búsqueda binario y luego copiará lavalue_type
parte del argumento al nodo. La ventaja de usarvalue_type
es que, bueno,value_type
siempre coincidevalue_type
, ¡no puedes escribir mal el tipo destd::pair
argumentos!La diferencia está en [3]. La función
std::make_pair
es una función de plantilla que creará unastd::pair
. La firma es:No he proporcionado intencionalmente los argumentos de la plantilla
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 casoT==K,U==V
, por lo que la llamada astd::make_pair
devolverá unstd::pair<K,V>
(tenga en cuenta lo que faltaconst
). La firma requierevalue_type
que esté cerca pero no sea lo mismo que el valor devuelto de la llamada astd::make_pair
. Debido a que está lo suficientemente cerca, creará un temporal del tipo correcto y la copia 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:
Pero eso sigue siendo propenso a errores de la misma manera que escribir explícitamente el tipo en el caso [1].
Hasta este punto, tenemos diferentes formas de llamar
insert
que requieren la creación de lovalue_type
externo y la copia de ese objeto en el contenedor. Alternativamente, puede usaroperator[]
si el tipo es por defecto construible y asignable (enfocando intencionalmente solo enm[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 reenvío perfecto, hay una nueva forma de agregar elementos a un contenedor mediante la colocación (creación en el lugar). Las
emplace
funciones 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 enviarán al constructor del objeto almacenado en el contenedor.En [5],
std::pair<const K, V>
no se crea ni se pasa aemplace
, sino que se pasan referencias al objetot
y que lo reenvía al constructor del subobjeto dentro de la estructura de datos. En este caso, no se realizan copias , lo cual es la ventaja de las alternativas de C ++ 03. Como en el caso , no anulará el valor en el mapa.u
emplace
value_type
std::pair<const K,V>
emplace
insert
Una pregunta interesante en la que no había pensado es cómo
emplace
se puede implementar realmente para un mapa, y ese no es un problema simple en el caso general.fuente
mapped_type
hay copia de la instancia. Lo que queremos es colocar la construcción delmapped_type
par en el par y colocar la construcción del par en el mapa. Por lo tanto, lastd::pair::emplace
función y su soporte de reenvío enmap::emplace
faltan. En su forma actual, aún tiene que dar un tipo_mapeado construido al constructor de pares que lo copiará, una vez. es mejor que dos veces, pero aún no es bueno.insert_or_assign
ytry_emplace
(ambos de C ++ 17), que ayudan a llenar algunos vacíos en la funcionalidad de los métodos existentes.Emplace: Aprovecha la referencia rvalue para usar los objetos reales que ya has creado. Esto significa que no se llama ningún constructor de copia o movimiento, ¡bueno para objetos GRANDES! O (log (N)) tiempo.
Insertar: tiene sobrecargas para la referencia estándar de lvalue y rvalue reference, así como iteradores para listas de elementos para insertar y "pistas" sobre la posición a la que pertenece un elemento. El uso de un iterador de "pista" puede hacer que la inserción de tiempo se reduzca al tiempo de contacto, de lo contrario es 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 hace 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 había "necesidad" de agregar un lugar al estándar. En c ++ 11 creo que se agregó el tipo de referencia &&. Esto eliminó la necesidad de mover la semántica y permitió la optimización de algún tipo específico de administración de memoria. En particular, la referencia de valor. 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 proporciona la capacidad de tratar con referencias de valor, ignora su propósito clave, que es la construcción de objetos.
fuente
emplace()
es simplemente la única forma de insertar un elemento que no se puede copiar o mover. (y sí, tal vez, para insertar de manera más eficiente uno cuyos constructores de copia y movimiento cuestan mucho más que la construcción, si existe tal cosa) También parece que te has equivocado: no se trata de " [aprovechar] la referencia de valor para usar los objetos reales que ya ha creado "; todavía no se crea ningún objeto y reenvíamap
los argumentos que necesita para crearlo dentro de sí mismo. No haces el objeto.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 esta última permite explícitamente conversiones . (Esto se encuentra en toda la biblioteca estándar, no solo para los mapas).
Aquí hay un ejemplo para demostrar:
Este es ciertamente un detalle muy específico, pero cuando se trata de cadenas de conversiones definidas por el usuario, vale la pena tener esto en cuenta.
fuente
v.emplace(v.end(), 10, 10);
... o ahora necesitarías usarv.emplace(v.end(), foo(10, 10) );
:?emplace
hacer uso de una clase que toma un solo parámetro. En mi opinión, en realidad haría que la naturaleza de la sintaxis variable de Emplace fuera mucho más clara si se usaran múltiples parámetros en los ejemplos.El siguiente código puede ayudarlo a comprender la "idea general" de cómo
insert()
difiere deemplace()
:El resultado que obtuve fue:
Darse cuenta de:
Un
unordered_map
siempre almacena internamenteFoo
objetos (y no, digamos,Foo *
s) como claves, que se destruyen cuandounordered_map
se destruye. Aquí, lasunordered_map
teclas internas de los foos 13, 11, 5, 10, 7 y 9.unordered_map
realmente almacenastd::pair<const Foo, int>
objetos, que a su vez almacenan losFoo
objetos. Pero para comprender la "idea general" de cómoemplace()
difiere deinsert()
(ver el cuadro resaltado a continuación), está bien imaginar temporalmente estestd::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 estestd::pair
objeto intermediariounordered_map
introduce tecnicismos sutiles, pero importantes.Insertar cada una de
foo0
,foo1
yfoo2
requirió 2 llamadas a uno deFoo
los constructores de copiar / mover y 2 llamadas aFoo
destructor (como describo ahora):a. Al insertar cada uno de ellos
foo0
yfoo1
crear un objeto temporal (foo4
yfoo6
, respectivamente) cuyo destructor fue llamado inmediatamente después de que se completó la inserción. Además, losFoo
s internos del unordered_map (que sonFoo
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 (llamadopair
), que llamóFoo
al constructor de copia enfoo2
(creandofoo8
como un miembro interno depair
). Luegoinsert()
editamos este par, lo que resultó enunordered_map
llamar nuevamente al constructor de copias (onfoo8
) para crear su propia copia interna (foo9
). Al igual que confoo
s 0 y 1, el resultado final fue dos llamadas de destructor para esta inserción, con la única diferencia de que esefoo8
destructor se llamó solo cuando llegamos al final de, enmain()
lugar de ser llamado inmediatamente después deinsert()
finalizar.El empalme
foo3
resultó en solo 1 llamada al constructor copiar / mover (creandofoo10
internamente enunordered_map
) y solo 1 llamada alFoo
destructor. (Volveré a esto más tarde).Para
foo11
, pasamos directamente el entero 11 aemplace(11, d)
para queunordered_map
llame alFoo(int)
constructor mientras la ejecución está dentro de suemplace()
método. A diferencia de (2) y (3), ni siquiera necesitábamos algúnfoo
objeto preexistente para hacer esto. Es importante destacar que solo seFoo
produjo 1 llamada a un constructor (que creófoo11
).Luego pasamos directamente el entero 12 a
insert({12, d})
. A diferencia de conemplace(11, d)
(cuya recuperación resultó en solo 1 llamada a unFoo
constructor), esta llamada ainsert({12, d})
resultó en dos llamadas alFoo
constructor (creaciónfoo12
yfoo13
).Esto muestra cuál es la principal diferencia de "panorama general" entre
insert()
yemplace()
es:Nota: La razón para el " casi " en " casi siempre " arriba se explica en I) a continuación.
umap.emplace(foo3, d)
llamadaFoo
es el siguiente: dado que estamos usandoemplace()
, el compilador sabe quefoo3
(unFoo
objeto no const ) está destinado a ser un argumento para algúnFoo
constructor. En este caso, elFoo
constructor más adecuado es el constructor de copia no constanteFoo(Foo& f2)
. Es por eso queumap.emplace(foo3, d)
llamó a un constructor de copia mientrasumap.emplace(11, d)
que no lo hizo.Epílogo:
I. Tenga en cuenta que una sobrecarga de en
insert()
realidad es equivalente aemplace()
. Como se describe en esta página de cppreference.com , la sobrecargatemplate<class P> std::pair<iterator, bool> insert(P&& value)
(que es la sobrecarga (2) deinsert()
en esta página de cppreference.com) es equivalente aemplace(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í ) yemplace()
(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é sobrecargainsert()
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: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
orstd::unordered_multiset
) en lugar destd::unordered_map
.C. Ahora use un
Goo
objeto (solo una copia renombrada deFoo
) en lugar de anint
como el tipo de rango en ununordered_map
(es decir, use enunordered_map<Foo, Goo>
lugar deunordered_map<Foo, int>
) y vea cuántos y a quéGoo
constructores se llaman. (Spoiler: hay un efecto pero no es muy dramático).fuente
En términos de funcionalidad o salida, ambos son iguales.
Para ambas memorias grandes, el emplazamiento de objetos está optimizado para la memoria y no utiliza constructores de copia
Para una explicación simple y detallada https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44
fuente