c++ - example - std map emplace
Forma preferida/idiomática para insertar en un mapa (8)
He identificado cuatro formas diferentes de insertar en un std::map
:
std::map<int, int> function;
function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));
¿Cuál de esas es la forma preferida / idiomática? (¿Y hay otra manera en la que no he pensado?)
A partir de C ++ 11, tienes dos opciones adicionales principales. Primero, puede usar insert()
con la sintaxis de inicialización de la lista:
function.insert({0, 42});
Esto es funcionalmente equivalente a
function.insert(std::map<int, int>::value_type(0, 42));
pero mucho más conciso y legible. Como otras respuestas han notado, esto tiene varias ventajas sobre las otras formas:
- El enfoque del
operator[]
requiere que el tipo mapeado sea asignable, lo que no siempre es el caso. - El enfoque del
operator[]
puede sobrescribir los elementos existentes y no le permite saber si esto sucedió. - Las otras formas de
insert
que enumera implican una conversión de tipo implícita, que puede ralentizar su código.
El mayor inconveniente es que este formulario solía requerir que la clave y el valor se puedan copiar, por lo que no funcionaría, por ejemplo, con un mapa con valores unique_ptr
. Eso se ha corregido en el estándar, pero es posible que la corrección no haya llegado a su implementación de biblioteca estándar.
En segundo lugar, puede usar el método emplace()
:
function.emplace(0, 42);
Esto es más conciso que cualquiera de las formas de insert()
, funciona bien con tipos de solo movimiento como unique_ptr
, y teóricamente puede ser ligeramente más eficiente (aunque un compilador decente debería optimizar la diferencia). El único inconveniente importante es que puede sorprender un poco a sus lectores, ya que los métodos de emplace
no suelen utilizarse de esa manera.
En primer lugar, el operator[]
y las funciones de miembro de insert
no son funcionalmente equivalentes:
- El
operator[]
buscará la clave, insertará un valor construido predeterminado si no se encuentra y devolverá una referencia a la que asigne un valor. Obviamente, esto puede ser ineficiente si elmapped_type
puede beneficiarse de ser inicializado directamente en lugar de predeterminado construido y asignado. Este método también hace que sea imposible determinar si realmente se ha realizado una inserción o si solo se sobrescribió el valor de una clave previamente insertada. - La función miembro de
insert
no tendrá efecto si la clave ya está presente en el mapa y, aunque a menudo se olvida, devuelve unstd::pair<iterator, bool>
que puede ser de interés (sobre todo para determinar si la inserción tiene realmente ha hecho).
De todas las posibilidades enumeradas para insert
llamadas, las tres son casi equivalentes. Como recordatorio, echemos un vistazo a la firma de insert
en el estándar:
typedef pair<const Key, T> value_type;
/* ... */
pair<iterator, bool> insert(const value_type& x);
Entonces, ¿cómo son las tres llamadas diferentes?
-
std::make_pair
basa en la deducción del argumento de la plantilla y podría (y en este caso) producir algo de un tipo diferente al valorvalue_type
del mapa, lo que requerirá una llamada adicional al constructor de la plantillastd::pair
para convertir avalue_type
(es decir, agregarconst
afirst_type
) -
std::pair<int, int>
también requerirá una llamada adicional al constructor de la plantilla destd::pair
para convertir el parámetro avalue_type
(es decir, agregarconst
afirst_type
) -
std::map<int, int>::value_type
deja absolutamente lugar a dudas ya que es directamente el tipo de parámetro esperado por la función miembro deinsert
.
Al final, evitaría usar el operator[]
cuando el objetivo es insertar, a menos que no haya un costo adicional en la construcción por defecto y la asignación de mapped_type
, y que no me preocupe determinar si una nueva clave se ha insertado efectivamente. Cuando se utiliza insert
, la construcción de un value_type
es probablemente el camino a seguir.
En resumen, el operador []
es más eficiente para actualizar valores porque implica llamar al constructor predeterminado del tipo de valor y luego asignarle un nuevo valor, mientras que insert()
es más eficiente para agregar valores.
El fragmento citado de Effective STL: 50 Formas específicas para mejorar el uso de la biblioteca de plantillas estándar por Scott Meyers, artículo 24 podría ayudar.
template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
typename MapType::iterator lb = m.lower_bound(k);
if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
lb->second = v;
return lb;
} else {
typedef typename MapType::value_type MVT;
return m.insert(lb, MVT(k, v));
}
}
Puede decidir elegir una versión libre de programación genérica de esto, y el punto es que creo que este paradigma (diferenciando ''agregar'' y ''actualizar'') es extremadamente útil.
He estado haciendo algunas comparaciones de tiempo entre las versiones mencionadas anteriormente:
function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));
Resulta que las diferencias de tiempo entre las versiones de inserción son pequeñas.
#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
Widget() {
m_vec.resize(100);
for(unsigned long it = 0; it < 100;it++) {
m_vec[it] = 1.0;
}
}
Widget(double el) {
m_vec.resize(100);
for(unsigned long it = 0; it < 100;it++) {
m_vec[it] = el;
}
}
private:
std::vector<double> m_vec;
};
int main(int argc, char* argv[]) {
std::map<int,Widget> map_W;
ptime t1 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
}
ptime t2 = boost::posix_time::microsec_clock::local_time();
time_duration diff = t2 - t1;
std::cout << diff.total_milliseconds() << std::endl;
std::map<int,Widget> map_W_2;
ptime t1_2 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_2.insert(std::make_pair(it,Widget(2.0)));
}
ptime t2_2 = boost::posix_time::microsec_clock::local_time();
time_duration diff_2 = t2_2 - t1_2;
std::cout << diff_2.total_milliseconds() << std::endl;
std::map<int,Widget> map_W_3;
ptime t1_3 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_3[it] = Widget(2.0);
}
ptime t2_3 = boost::posix_time::microsec_clock::local_time();
time_duration diff_3 = t2_3 - t1_3;
std::cout << diff_3.total_milliseconds() << std::endl;
std::map<int,Widget> map_W_0;
ptime t1_0 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
}
ptime t2_0 = boost::posix_time::microsec_clock::local_time();
time_duration diff_0 = t2_0 - t1_0;
std::cout << diff_0.total_milliseconds() << std::endl;
system("pause");
}
Esto da respectivamente para las versiones (ejecuté el archivo 3 veces, de ahí las 3 diferencias de tiempo consecutivas para cada una):
map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
2198 ms, 2078 ms, 2072 ms
map_W_2.insert(std::make_pair(it,Widget(2.0)));
2290 ms, 2037 ms, 2046 ms
map_W_3[it] = Widget(2.0);
2592 ms, 2278 ms, 2296 ms
map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
2234 ms, 2031 ms, 2027 ms
Por lo tanto, los resultados entre diferentes versiones de inserción se pueden descuidar (¡aunque no se realizó una prueba de hipótesis)!
El map_W_3[it] = Widget(2.0);
la versión tarda aproximadamente un 10-15% más de tiempo en este ejemplo debido a una inicialización con el constructor predeterminado para Widget.
La primera versión:
function[0] = 42; // version 1
puede o no insertar el valor 42 en el mapa. Si la clave 0
existe, entonces asignará 42 a esa tecla, sobrescribiendo el valor que tenga esa clave. De lo contrario, inserta el par clave / valor.
Las funciones de inserción:
function.insert(std::map<int, int>::value_type(0, 42)); // version 2
function.insert(std::pair<int, int>(0, 42)); // version 3
function.insert(std::make_pair(0, 42)); // version 4
por otro lado, no haga nada si la clave 0
ya existe en el mapa. Si la clave no existe, inserta el par clave / valor.
Las tres funciones de inserción son casi idénticas. std::map<int, int>::value_type
es typedef
para std::pair<const int, int>
, y std::make_pair()
obviamente produce un std::pair<>
través de la magia de deducción de plantillas. El resultado final, sin embargo, debería ser el mismo para las versiones 2, 3 y 4.
¿Cuál usaría? Personalmente prefiero la versión 1; es conciso y "natural". Por supuesto, si su comportamiento de sobreescritura no es deseado, entonces preferiría la versión 4, ya que requiere menos tipeo que las versiones 2 y 3. No sé si existe una única forma de hecho de insertar pares clave / valor en una std::map
.
Otra forma de insertar valores en un mapa a través de uno de sus constructores:
std::map<int, int> quadratic_func;
quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;
std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());
Si desea sobrescribir el elemento con la clave 0
function[0] = 42;
De otra manera:
function.insert(std::make_pair(0, 42));
Si quiere insertar un elemento en std :: map - use la función insert (), y si quiere encontrar un elemento (por clave) y asignarle algo - use el operador [].
Para simplificar, inserte use boost :: assign library, así:
using namespace boost::assign;
// For inserting one element:
insert( function )( 0, 41 );
// For inserting several elements:
insert( function )( 0, 41 )( 0, 42 )( 0, 43 );
Solo cambio el problema un poco (mapa de cadenas) para mostrar otro interés de inserción:
std::map<int, std::string> rancking;
rancking[0] = 42; // << some compilers [gcc] show no error
rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error
el hecho de que el compilador no muestra ningún error en "rancking [1] = 42;" puede tener un impacto devastador!