¿Por qué std :: map operator crea un objeto si la clave no existe?

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 operator [] 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 locking (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 obtuve muchas respuestas que básicamente dicen que 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 [] 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 sería más intuitivo de esa manera.

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

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.

El estándar dice (23.3.1.2/1) que el operador [] regresa (*((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 así?

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 existe un rango claro predefinido de claves que deben 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 inservible e insertada. 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 un operador de inserción y proporcionar una función para la búsqueda no insertable. Entonces, alguien alguna vez decidió seguir este último enfoque. Eso es todo lo que hay.

Si lo hicieron al revés, tal vez hoy alguien estaría preguntando aquí la versión inversa de su pregunta.

Es para fines de asignación:

 void test() { std::mapmyMap; myMap["hello"] = 5; } 

Permite la inserción de nuevos elementos con el operator[] , así:

 std::map m; m["five"] = 5; 

El 5 se asigna al valor devuelto por m["five"] , que es una referencia a un elemento creado recientemente. Si el operator[] no insertara elementos nuevos, esto no podría funcionar de esa manera.

Creo que es sobre todo porque en el caso del mapa (a diferencia del vector, por ejemplo) es bastante barato y fácil de hacer, solo tienes que crear un elemento único. En el caso del vector, podrían extender el vector para hacer que un nuevo subíndice sea válido, pero si su nuevo subíndice va más allá de lo que ya existe, agregar todos los elementos hasta ese punto puede ser bastante caro. Cuando extiende un vector, también normalmente especifica los valores de los nuevos elementos que se agregarán (aunque a menudo con un valor predeterminado). En este caso, no habría forma de especificar los valores de los elementos en el espacio entre los elementos existentes y el nuevo.

También hay una diferencia fundamental en cómo se usa un mapa de forma típica. 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 código que manipula el elemento que está allí si hay uno, o agrega un nuevo elemento si aún no está allí. El diseño del operador [] para cada uno refleja eso.

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 solicita 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 comprobación de límites se consideró un costo innecesario para quienes no lo deseaban. En el caso de los mapas, la única forma de no devolver una referencia es lanzar una excepción y también se consideró un costo innecesario para quienes no lo deseaban.

map.insert (clave, elemento); se asegura de que la clave esté en el mapa pero no sobrescribe un valor existente.

map.operator [clave] = elemento; se asegura de que la clave esté en el mapa y sobrescribe 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.

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 en las segundas líneas de los tres bloques)

Mira qué bonito es este código:

 int main () { int n, curr; map 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 < 

De acuerdo con el operator[] estándar operator[] devuelve la referencia en (*((insert(make_pair(key, T()))).first)).second . Es por eso que podría 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 lindo, ¿verdad? Este es un buen ejemplo de que es realmente conveniente.

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 ...