Why does std::map operator[] create an object if the key doesn't exist?

回眸只為那壹抹淺笑 提交于 2019-11-26 22:38:48
R Samuel Klatchko

Because operator[] returns a reference to the value itself and so the only way to indicate a problem would be to throw an exception (and in general, the STL rarely throws exceptions).

If you don't like this behavior, you can use map::find instead. It returns an iterator instead of the value. This allows it to return a special iterator when the value is not found (it returns map::end) but also requires you to dereference the iterator to get at the value.

Standard says (23.3.1.2/1) that operator[] returns (*((insert(make_pair(x, T()))).first)).second. That's the reason. It returns reference T&. There is no way to return invalid reference. And it returns reference because it is very convenient I guess, isn't it?

To answer your real question: there's no convincing explanation as to why it was done that way. "Just because".

Since std::map is an associative container, there's no clear pre-defined range of keys that must exist (or not exist) in the map (as opposed to the completely different situation with std::vector). That means that with std::map, you need both non-insering and inserting lookup functionality. One could overload [] in non-inserting way and provide a function for insertion. Or one could do the other way around: overload [] as an inserting operator and provide a function for non-inserting search. So, someone sometime decided to follow the latter approach. That's all there's to it.

If they did it the other way around, maybe today someone would be asking here the reverse version of your question.

Its is for assignment purposes:


void test()
{
   std::map<std::string, int >myMap;
   myMap["hello"] = 5;
}

I think it's mostly because in the case of map (unlike vector, for example) it's fairly cheap and easy to do -- you only have to create a single element. In the case of vector they could extend the vector to make a new subscript valid -- but if your new subscript is well beyond what's already there, adding all the elements up to that point may be fairly expensive. When you extend a vector you also normally specify the values of the new elements to be added (though often with a default value). In this case, there would be no way to specify the values of the elements in the space between the existing elements and the new one.

There's also a fundamental difference in how a map is typically used. With a vector, there's usually a clear delineation between things that add to a vector, and things that work with what's already in the vector. With a map, that's much less true -- it's much more common to see code that manipulates the item that's there if there is one, or adds a new item if it's not already there. The design of operator[] for each reflects that.

It allows insertion of new elements with operator[], like this:

std::map<std::string, int> m;
m["five"] = 5;

The 5 is assigned to the value returned by m["five"], which is a reference to a newly created element. If operator[] wouldn't insert new elements this couldn't work that way.

map.insert(key, item); makes sure key is in the map but does not overwrite an existing value.

map.operator[key] = item; makes sure key is in the map and overwrites any existing value with item.

Both of these operations are important enough to warrant a single line of code. The designers probably picked which operation was more intuitive for operator[] and created a function call for the other.

The difference here is that map stores the "index", i.e. the value stored in the map (in its underlying RB tree) is a std::pair, and not just "indexed" value. There's always map::find() that would tell you if pair with a given key exists.

The answer is because they wanted an implementation that is both convenient and fast.

The underlying implementation of a vector is an array. So if there are 10 entries in the array and you want entry 5, the T& vector::operator[](5) function just returns headptr+5. If you ask for entry 5400 it returns headptr+5400.

The underlying implementation of a map is usually a tree. Each node is allocated dynamically, unlike the vector which the standard requires to be contiguous. So nodeptr+5 doesn't mean anything and map["some string"] doesn't mean rootptr+offset("some string").

Like find with maps, vector has getAt() if you want bounds checking. In the case of vectors, bounds checking was considered an unnecessary cost for those who did not want it. In the case of maps, the only way not to return a reference is to throw an exception and that was also considered an unnecessary cost for those who did not want it.

Consider such an input - 3 blocks, each block 2 lines, first line is the number of elements in the second one:

5
13 20 22 43 146
4
13 22 43 146
5
13 43 67 89 146

Problem: calculate the number of integers that are present in second lines of all three blocks. (For this sample input the output should be 3 as far as 13, 43 and 146 are present in second lines of all three blocks)

See how nice is this code:

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;
}

According to the standard operator[] returns reference on (*((insert(make_pair(key, T()))).first)).second. That is why I could write:

myMap[curr]++;

and it inserted an element with key curr and initialized the value by zero if the key was not present in the map. And also it incremented the value, in spite of the element was in the map or no.

See how simple? It is nice, isn't it? This is a good example that it is really convenient.

Clemens

It it not possible to avoid the creation of an object, because the operator[] doesn't know how to use it.

myMap["apple"] = "green";

or

char const * cColor = myMyp["apple"];

I propose the map container should add an function like

if( ! myMap.exist( "apple")) throw ...

it is much simpler and better to read than

if( myMap.find( "apple") != myMap.end()) throw ...

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!