c++ - ejemplo - std::map example
¿Por qué std:: map operator[] crea un objeto si la clave no existe? (11)
Estoy bastante seguro de que ya vi esta pregunta en alguna parte (comp.lang.c ++? Google tampoco parece encontrarla allí), pero una búsqueda rápida aquí no parece encontrarla, así que aquí está:
¿Por qué el operador std :: map [] crea un objeto si la clave no existe? No sé, pero para mí esto parece contrario a la intuición si se compara con la mayoría de los otros operadores [] (como std :: vector), donde si lo usa debe asegurarse de que el índice exista. Me pregunto cuál es la razón para implementar este comportamiento en std :: map. Como dije, ¿no sería más intuitivo actuar más como un índice en un vector y un bloqueo (un comportamiento bien indefinido, supongo) cuando se accede con una clave no válida?
Refinando mi pregunta después de ver las respuestas:
Ok, hasta ahora tengo muchas respuestas que dicen que básicamente es barato, ¿por qué no o cosas similares? Estoy totalmente de acuerdo con eso, pero ¿por qué no utilizar una función dedicada para eso (creo que uno de los comentarios dice que en Java no hay operador [] y la función se llama put)? Mi punto es ¿por qué el operador de mapas [] no funciona como un vector? Si utilizo el operador [] en un índice fuera de rango en un vector, no me gustaría que inserte un elemento , aunque sea barato, porque eso probablemente signifique un error en mi código. Mi punto es por qué no es lo mismo con el mapa. Quiero decir, para mí, usar operator [] en un mapa significaría: sé que esta clave ya existe (por alguna razón, acabo de insertarla, tengo redundancia en alguna parte, lo que sea). Creo que así sería más intuitivo.
Dicho esto, ¿cuál es la ventaja de hacer el comportamiento actual con el operador [] (y solo para eso, estoy de acuerdo en que una función con el comportamiento actual debería estar allí, pero no el operador [])? Tal vez da un código más claro de esa manera? No lo sé.
Otra respuesta fue que ya existía de esa manera, ¿por qué no mantenerlo pero luego, probablemente cuando ellos (los que estaban antes de stl) optaron por implementarlo de esa manera, encontraron que proporcionaba una ventaja o algo así? Así que mi pregunta es básicamente: ¿por qué elegir implementarlo de esa manera, lo que significa una cierta falta de coherencia con otro operador []. ¿Qué beneficio le da?
Gracias
Considere tal entrada - 3 bloques, cada bloque 2 líneas, la primera línea es la cantidad de elementos en el segundo:
5
13 20 22 43 146
4
13 22 43 146
5
13 43 67 89 146
Problema: calcule el número de enteros que están presentes en las segundas líneas de los tres bloques. (Para esta entrada de muestra, la salida debe ser 3 hasta 13, 43 y 146 están presentes en las segundas líneas de los tres bloques)
Mira lo bonito que es este código:
int main ()
{
int n, curr;
map<unsigned, unsigned char> myMap;
for (int i = 0; i < 3; ++i)
{
cin >> n;
for (int j = 0; j < n; ++j)
{
cin >> curr;
myMap[curr]++;
}
}
unsigned count = 0;
for (auto it = myMap.begin(); it != myMap.end(); ++it)
{
if (it->second == 3)
++count;
}
cout << count <<endl;
return 0;
}
De acuerdo con el operator[]
estándar operator[]
devuelve la referencia en (*((insert(make_pair(key, T()))).first)).second
. Por eso pude escribir:
myMap[curr]++;
e insertó un elemento con key curr
e inicializó el valor en cero si la clave no estaba presente en el mapa. Y también incrementó el valor, a pesar de que el elemento estaba en el mapa o no.
¿Ves lo simple? Es bueno, ¿no? Este es un buen ejemplo de que es realmente conveniente.
Creo que es principalmente porque en el caso del mapa (a diferencia de vector, por ejemplo) es bastante barato y fácil de hacer, solo tienes que crear un único elemento. En el caso de vector, podrían extender el vector para hacer que un nuevo subíndice sea válido, pero si su nuevo subíndice está más allá de lo que ya existe, agregar todos los elementos hasta ese punto puede ser bastante costoso. Cuando extiende un vector, normalmente también especifica los valores de los nuevos elementos que se agregarán (aunque a menudo con un valor predeterminado). En este caso, no habría manera de especificar los valores de los elementos en el espacio entre los elementos existentes y el nuevo.
También hay una diferencia fundamental en la forma en que normalmente se usa un mapa. Con un vector, generalmente hay una delineación clara entre las cosas que se agregan a un vector y las cosas que funcionan con lo que ya está en el vector. Con un mapa, eso es mucho menos cierto: es mucho más común ver el código que manipula el elemento que está ahí si lo hay, o agrega un nuevo elemento si aún no está allí. El diseño del operador [] para cada uno refleja eso.
El estándar dice (23.3.1.2/1) que el operador [] devuelve (*((insert(make_pair(x, T()))).first)).second
. Esa es la razón. Devuelve la referencia T&
. No hay forma de devolver la referencia no válida. Y devuelve referencia porque es muy conveniente, supongo, ¿no?
Es para fines de asignación:
void test()
{
std::map<std::string, int >myMap;
myMap["hello"] = 5;
}
La diferencia aquí es que el mapa almacena el "índice", es decir, el valor almacenado en el mapa (en su árbol RB subyacente) es un valor std::pair
, y no solo "indexado". Siempre hay un map::find()
que le diría si existe un par con una clave dada.
La respuesta es porque querían una implementación que fuera conveniente y rápida.
La implementación subyacente de un vector es una matriz. Entonces, si hay 10 entradas en la matriz y desea la entrada 5, la función T & vector :: operator [] (5) simplemente devuelve headptr + 5. Si pides la entrada 5400 devuelve headptr + 5400.
La implementación subyacente de un mapa suele ser un árbol. Cada nodo se asigna dinámicamente, a diferencia del vector que requiere el estándar para ser contiguo. Así que nodeptr + 5 no significa nada y map ["some string"] no significa rootptr + offset ("some string").
Al igual que con los mapas, vector tiene getAt () si desea comprobar los límites. En el caso de los vectores, la verificación de límites se consideró un costo innecesario para quienes no lo querían. En el caso de los mapas, la única forma de no devolver una referencia es lanzar una excepción y eso también se consideró un costo innecesario para quienes no lo querían.
No es posible evitar la creación de un objeto, porque el operador [] no sabe cómo usarlo.
myMap["apple"] = "green";
o
char const * cColor = myMyp["apple"];
Propongo que el contenedor del mapa debe agregar una función como
if( ! myMap.exist( "apple")) throw ...
es mucho más simple y mejor para leer que
if( myMap.find( "apple") != myMap.end()) throw ...
Para responder a su pregunta real: no hay una explicación convincente de por qué se hizo de esa manera. "Simplemente porque".
Dado que std::map
es un contenedor asociativo , no hay un rango claro de teclas predefinidas que deban existir (o no existir) en el mapa (a diferencia de la situación completamente diferente con std::vector
). Eso significa que con std::map
, necesita funcionalidad de búsqueda no inserta e inserta. Uno podría sobrecargar []
de forma no insertable y proporcionar una función para la inserción. O se podría hacer al revés: sobrecargar []
como operador de inserción y proporcionar una función para la búsqueda sin inserción. Entonces, alguien en algún momento decidió seguir este último enfoque. Eso es todo lo que hay que hacer.
Si lo hicieron al revés, tal vez hoy alguien estaría preguntando aquí la versión inversa de su pregunta.
Permite la inserción de nuevos elementos con el operator[]
, así:
std::map<std::string, int> m;
m["five"] = 5;
El 5
se asigna al valor devuelto por m["five"]
, que es una referencia a un elemento recién creado. Si el operator[]
no insertara nuevos elementos, esto no podría funcionar de esa manera.
Porque el operator[]
devuelve una referencia al valor mismo y, por lo tanto, la única forma de indicar un problema sería lanzar una excepción (y, en general, el STL raramente arroja excepciones).
Si no te gusta este comportamiento, puedes usar map::find
lugar. Devuelve un iterador en lugar del valor. Esto le permite devolver un iterador especial cuando no se encuentra el valor (devuelve map::end
) pero también requiere que desreferencia el iterador para obtener el valor.
map.insert (clave, elemento); se asegura de que la clave esté en el mapa pero no sobrescriba un valor existente.
map.operator [clave] = elemento; se asegura de que la clave esté en el mapa y sobrescriba cualquier valor existente con el elemento.
Ambas operaciones son lo suficientemente importantes como para garantizar una sola línea de código. Los diseñadores probablemente eligieron qué operación era más intuitiva para el operador [] y crearon una llamada de función para la otra.