41

I'm pretty sure I already saw this question somewhere (comp.lang.c++? Google doesn't seem to find it there either), but a quick search here doesn't seem to find it, so here it is:

Why does std::map operator[] create an object if the key doesn't exist? I don't know but for me this seems counter-intuitive if you compare to most other operator[] (like std::vector) where if you use it you must be sure that the index exists. I'm wondering what's the rationale for implementing this behavior in std::map. Like I said wouldn't it be more intuitive to act more like an index in a vector and crash (well undefined behavior I guess) when accessed with an invalid key?

I've gotten a lot of answers saying basically "it's cheap, so why not?" or similar. I totally agree with that, but why not use a dedicated function for that (I think one of the comments said that in Java there is no operator[] and the function is called put)? My point is why doesn't map operator[] work like a vector? If I use operator[] on an out of range index on a vector I wouldn't like it to insert an element even if it was cheap because that probably mean an error in my code. My point is why isn't it the same thing with map. I mean, for me, using operator[] on a map would mean: i know this key already exist (for whatever reason, I just inserted it, I have redundancy somewhere, whatever). I think it would be more intuitive that way.

That said what are the advantage of doing the current behavior with operator[] (and only for that, I agree that a function with the current behavior should be there, just not operator[])? Maybe it give clearer code that way? I don't know.

Another answer was that it already existed that way so why not keep it but then, probably when they (the ones before stl) choose to implement it that way they found it provided an advantage or something? So my question is basically: why choose to implement it that way, meaning a somewhat lack of consistency with other operator[]. What benefit do it give?

TylerH
  • 20,799
  • 66
  • 75
  • 101
n1ckp
  • 1,481
  • 1
  • 14
  • 21
  • 1
    For std::vector the operator[] is unchecked. If you attempt to access outside the range of the vector it happily returns a reference to an invalid object. If you need checking use the method at(). On the other hand std::map can't do that. The easy work around for map is just to use the find() method. – Martin York Oct 28 '09 at 19:47
  • 2
    The std::vector<> operator[] isn't guaranteed to be unchecked, it just isn't required to be checked. – David Thornley Oct 28 '09 at 20:45
  • @David Thornley: Thanks I didn't know that. – n1ckp Oct 28 '09 at 20:58
  • 4
    I would just like that they have a `const` version of the subscription operator, I am ready to pay the price of an exception to avoid the use of `map::find` + `map::end`, really. – Matthieu M. Oct 29 '09 at 08:16
  • @MatthieuM. agreed. Though, you can use `map::count` instead. – anilbey Aug 27 '19 at 14:01
  • 1
    Yes, this is one of the dumbest features of STL. Most of us think of [] as a read operation, but it is also used for writing by returning a non-const reference (to which you can then assign). You wouldn't want the reference to be invalid, because people would routinely shoot themselves in the foot. One way around this (as Klatchko points out) would have been to have operator[] throw an exception. That would have been my preferred design, but STL seems allergic toward exceptions. This is what Python does (and it doesn't even need to worry about invalid references), and it Just Makes Sense(TM). – allyourcode Jun 02 '21 at 05:31
  • @allyourcode: Python solves the issue by having distinct `__getitem__` and `__setitem__` special methods. The C++ equivalent would be having separately overloadable `operator[](key) const` and `operator[]=(key, value)` functions. – dan04 Jan 05 '22 at 21:25
  • So it can be "the greatest newbie-trap in the C++ language" :) https://youtu.be/lkgszkPnV8g?t=423 – Jeremy Friesner Apr 15 '22 at 18:31

13 Answers13

26

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.

JFMR
  • 23,265
  • 4
  • 52
  • 76
R Samuel Klatchko
  • 74,869
  • 16
  • 134
  • 187
  • 1
    but doesn't vector return a reference too? what's the difference here? Sorry I'm not familiar with std::map's internal so maybe this is obvious from the internal. – n1ckp Oct 28 '09 at 19:43
  • 3
    It is undefined behaviour to access a vector out of bounds with []. I guess map could invoke some undefined behaviour in this case as well, but would you really like that? With a vector it is easy to find out if an index is valid, with a map it isn't (you could call find first, and in this case it would be pointless to call[] as you should have already found the item you are looking for). – UncleBens Oct 28 '09 at 19:47
  • @n1ck: std::vector will return an invalid reference (becuase it is easy todo). std::map does not have an easy way to return a reference. – Martin York Oct 28 '09 at 19:48
  • @UncleBens: yes I guess what you say make sense. If operator[] was working like I would find more intuitive, it would basically only be a find. Thanks, I didn't realise that. Still I don't like the way it works. I know that I don't have to use it if I don't like it but I asked this question because I just replaced a vector with a map, counting on the compiler to find me the errors and I got a runtime error because of this behavior. I guess that's what I get for counting on the compiler to find my errors when I could have done better ;o) – n1ckp Oct 28 '09 at 19:58
  • 13
    Traditionally, the interface for maps (in all languages, not just C++) allow you to write something like `map[key] = value;`, and the map would replace the existing value _or_ insert a new one as needed. It's a long standing convention, so it's no surprise that `std::map` follows it. However, since `operator[]` can't tell if the reference it has to return will be used on left side of `operator=` or not, it has to always assume the worst, and therefore insert a new element. – Pavel Minaev Oct 28 '09 at 20:18
  • @Pavel Minaev: Thanks I didn't know that other language before c++ implemented it that way. Any exemple? – n1ckp Oct 28 '09 at 20:27
  • C#, VB, Delphi, Python, Ruby, Eiffel, JavaScript, Lua are among those which specifically have `[]` notation or a precise equivalent (though in all those languages, the map can distinguish between retrieval of value via `[]` and assignment to `[]`, so this issue is non-existent there). Java doesn't have `[]`, but `Map.put` has similar insert-or-replace semantics. In reality, the complete list would probably be several pages old - it's a very old convention. This particular wart with default-initialization is specific to C++ only because there's no good way to distinguish assignment. – Pavel Minaev Oct 28 '09 at 20:35
  • Most of these language are not older than c++ so the decision cannot come from doing like them. But it's ok I'm sure you're right I was just curious which language could have inspired c++ for that decision. – n1ckp Oct 28 '09 at 20:46
  • 2
    @N1ck: They might be older that the std library, though. `:)` – sbi Oct 28 '09 at 20:48
  • I often use `map::count()` for that purpose. – anilbey Aug 27 '19 at 13:55
14

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?

Kirill V. Lyadvinsky
  • 97,037
  • 24
  • 136
  • 212
  • Sorry, maybe my question wasn't clear enought but I'm asking the reason for the standard. – n1ckp Oct 28 '09 at 19:36
  • 2
    Ok, it is convenient that `operator[]` returns reference. So it should insert something, because there is no way to return invalid reference. – Kirill V. Lyadvinsky Oct 28 '09 at 19:37
  • Look at Samuel's answer: http://stackoverflow.com/questions/1639544/1639573#1639573 – sbi Oct 28 '09 at 19:37
  • 1
    Why don't they make a const version that throws an exception? – GManNickG Oct 28 '09 at 19:40
  • 2
    Because exception is for exceptional cases. – Kirill V. Lyadvinsky Oct 28 '09 at 19:41
  • @GMan: If the map's `operator[]` wouldn't create values, it would be a lookup function. And you don't want your lookup function to throw if it doesn't find what you're looking for, do you? – sbi Oct 28 '09 at 19:42
  • 1
    @sbi: unless you aldready know that it exist? But yeah I just realised that if it worked like I wanted it would basically be a find, thanks. – n1ckp Oct 28 '09 at 20:00
  • 2
    Pretty much along nicks thought. Ever time I've ever used a map for look-up, I've had to end up checking if find returned end, then throw an exception, because it's the only appropriate thing to do. – GManNickG Oct 28 '09 at 20:02
  • @n1ck: If you already know that it exists, then what's wrong with `operator[]` as it is? – sbi Oct 28 '09 at 21:06
  • @GMan: Look at my comments to this question: http://stackoverflow.com/questions/1462341/ – sbi Oct 28 '09 at 21:08
  • because it would assert in debug mode if it didn't instead of just creating an (possibly invalid since second element is possibly default constructed) element without telling me, meaning I have to catch the error later. – n1ckp Oct 28 '09 at 21:15
  • @n1ck: In debug mode, an additional `assert(my_map.find(key)!=my_map.end());` would do. – sbi Oct 29 '09 at 09:37
  • I know but it's more code. I guess I could always add a wrapper class that provide the behavior I want if I really wanted to. What really caused me to ask is that I replaced a vector with a map, counting on the compiler to find me errors (didn't think about map's []) and instead of asserting right when I accessed an invalid index (my key was an int) it crashed later when I tried to use the (invalid) element it had created. It was not the first time I had problem with this behavior of map so I decided to ask the question. Seeing the answers I guess I'm the only one who do. ... – n1ckp Oct 29 '09 at 11:13
  • Anyway thanks for the discussion it made me found some point I hadn't thought about. Still I stay by my point that this behavior of operator[] is unintuitive compared to vector's. – n1ckp Oct 29 '09 at 11:18
7

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.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • I guess this is what I wanted to know, Thanks. Well a third way would have been not to provide an operator[] and provide lookup and insertion in two member function (no operator overloading). – n1ckp Oct 28 '09 at 20:17
  • @n1ck: The [] notation is pretty well fixed in people's minds. It would have been theoretically possible to not use it, but I don't think that was very likely. – David Thornley Oct 28 '09 at 20:47
  • The reason given above by Pavel Minaev is pretty convincing. –  Oct 29 '09 at 11:48
  • 1
    Which one? That this is a "long standing convention"? Might be true, but not convincing. That there's no way to know whether it is on the left-hand side of assignment? This is not convincing at all, since it could just throw an exception or claim undefined behavior. Remember, the original question was why it is different from `std::vector`. – AnT stands with Russia Oct 29 '09 at 13:47
  • See a few convincing explanations e.g. in the comments under [this answer](https://stackoverflow.com/a/1639573/1479945). – Sz. Jul 14 '19 at 10:35
4

Its is for assignment purposes:


void test()
{
   std::map<std::string, int >myMap;
   myMap["hello"] = 5;
}
EToreo
  • 2,936
  • 4
  • 30
  • 36
  • 1
    If this was the main reason, then a constant map would return a constant reference which cannot be assigned to and `operator[]` for constant maps wouldn't have to create a value. But there isn't even an `operator[]` for constant maps, yo you must be wrong. – sbi Oct 28 '09 at 19:40
  • 1
    I'm not sure how that makes me wrong... it seems like if there is no
    operator[]
    on a constant map, it just proves my point.
    – EToreo Oct 28 '09 at 20:17
  • 1
    That there is no `operator[]` for constant maps seems to indicate that `operator[]` is there *purely* to modify maps. I somehow must be misunderstanding the point you're making. If the designers of the STL didn't think that an `operator[]` of any kind would make sense for constant maps, they obviously saw the whole meaning of `operator[]` in modifying the map, like with the assignment in the answer... – sth Oct 28 '09 at 20:18
  • 1
    Having `operator[]` have two vastly different semantics depending on whether the map is `const` or not sounds like a bad idea to me. The way it is, `[]` has certain well-defined semantics which is simply not supported for `const` map - hence it's not there. – Pavel Minaev Oct 28 '09 at 20:19
  • @EToreo & sth: I can see that my point was somewhat weak. `` Let me... As it is, `operator[]` serves two purposes: You can use it to access an existing element and you can insert elements. There are specialized functions for both, so presumably it's the combination, why `operator[]` exists. For the add semantics you have `insert()`, for the access semantics you have `find()` - except `find()` can return "non-existing element", while the operator cannot. The insertion semantics is needed for _both adding and lookup_ to work using `operator[]`. – sbi Oct 28 '09 at 21:04
4

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Well I'm not sure if adding a new element in a vector when the index is out of range without intervention from the user would exactly be the behavior I want. If I access an out-of-bound array it is probably because I made an error somewhere in my code. I don't want the language to create one for me. I give you +1 for the second part although that's not really my personal experience with maps. – n1ckp Oct 28 '09 at 20:48
3

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.

sth
  • 222,467
  • 53
  • 283
  • 367
  • see my comment at http://stackoverflow.com/questions/1639544/1639563#1639563 for why I believe this is wrong. – sbi Oct 28 '09 at 19:40
3

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.

me.
  • 31
  • 1
1

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.

Nikolai Fetissov
  • 82,306
  • 11
  • 110
  • 171
1

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.

jmucchiello
  • 18,754
  • 7
  • 41
  • 61
  • Couldn't they return an invalid reference like if you dereferenced std::map' end()? – n1ckp Oct 28 '09 at 20:32
  • 1
    @n1ck: No. There are no invalid references in C++. (That's one of the main things where they differ from pointers.) – sbi Oct 28 '09 at 20:50
  • 1
    n1ck, think about the logic flow. To insert, the program has to find what to put the entry in the tree. This processing has a cost. Since the most likely action after not finding the value is to insert it, why not just have \\[\\] do the insert automagically. – jmucchiello Oct 28 '09 at 20:59
  • What then if you dereference something like std::map's end? or if if dereference an invalid pointer? The same behavior could have been implemented: if it doesn't exist: undefined behavior. – n1ckp Oct 28 '09 at 21:01
  • @jmucchiello: Good comments. I just find the use of operator[] unintuitive for that purpose but yeah your kinda right. – n1ckp Oct 28 '09 at 21:02
  • @n1ck: The difference is that it's relatively easy and cheap to check whether an iterator equals `end()` or whether an index is in the right range. It's more expensive to check for the existence of an entry in a map. But that doesn't mean you cannot do this before invoking `operator[]`. – sbi Oct 29 '09 at 09:40
  • I somewhat totally agree. My point is that why operator[]. I'm not saying a function that do this behavior should not exist, but why operator[]? That's somewhat the only thing we disagree I think. A good answer was that it was a convention before so why not keep it. My question then: why did THEY choose it then? – n1ckp Oct 29 '09 at 11:21
0

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.

Narek
  • 38,779
  • 79
  • 233
  • 389
  • Hmmm yeah this might be convenient and nice but why use operator[] and not a more explicit named function? The behavior do not look intuitive to me and that's why I find it strange to be the behavior of something like operator[] instead of using a function with an explicit name for something that do some weird processing that's not directly intuitive. I'm not saying it isn't nice to have it but why do operator[] has this behavior, it could have been a standard function. – n1ckp May 17 '13 at 19:04
  • I guess you have a good experience on other programming language, don't you? :) – Narek May 18 '13 at 08:46
  • yes and no, why do you ask? I'm mainly a C++ guy but I do know a couple other languages. – n1ckp May 21 '13 at 21:08
  • I thought that you are not a C++ guy that's why you ask this question. But seems there is another reason :) – Narek May 21 '13 at 21:14
  • The reason I asked the question is because the behavior is not intuitive and I wanted to know WHY it was coded this way. I of course knew HOW it acted, so I had no problem with that. Thanks for your answer but this is not what I was looking for. – n1ckp May 23 '13 at 19:17
  • To understand that just think what if it was not as is. Would it be more convenient? – Narek May 25 '13 at 11:16
  • In my opinion, yes, it would be more intuitive and convenient to not create the object as operator[] is an indexing operator. I think it is weird for it to create the object if it do not exist. This is how it works with vector, so why not with map? – n1ckp May 27 '13 at 17:01
0

I know this is old question but no one seems to have answered it well IMO. So far I haven't seen any mention of this:

The possibility of undefined behavior is to be avoided! If there is any reasonable behavior besides UB, then I imagine we should go with that.

std::vector/array exhibits undefined behavior with a bad operator[] index because there is really no reasonable option, since this is one of the fastest, most fundamental things you can do in c/c++, and it would be wrong to try to check anything. Checking is what at() is for.

std::*associative_container* has already done the work of finding where an indexed element would go, so it makes sense to create one there and return it. This is very useful behavior, and alternatives to operator[] are much less clean looking, but even if creating and inserting a new item is not what you wanted, or is not useful to you, it is still a much better result than undefined behavior.

I think operator[] is much preferred syntax for using an associative container, for readability, and to me this is very intuitive, and matches exactly the concept of operator[] for arrays: return a reference to the item at that position, to use or to assign to.

If my intuition for "what if there is nothing there" was only "undefined behavior", then I would be absolutely no worse off, since I would be doing all I could do avoid that, full stop.

Then one day I find out that I can insert an item with operator[]... life is just better.

Orion
  • 1
  • 1
-1

If you want to read an element with some key from an std::map,
but you are unsure whether it exists,
and in case it doesn't, you don't want to insert it by accident,
but rather want to get an exception thrown,
but you also don't want to manually check map.find(key) != map.end() everytime you read an element,

just use map::at(key) (C++11)

https://www.cplusplus.com/reference/map/map/at/

euphrat
  • 146
  • 7
-2

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

Ethan Heilman
  • 16,347
  • 11
  • 61
  • 88
Clemens
  • 55
  • 3
  • Not sure what you mean "because the operator[] doesn't know how to use it". If you read my answer to the other answers, like I said, my first expectation would have been to act like vector and crash on invalid index. i.e. vector v; v[1] = 1; will crash, why not with map? – n1ckp Jul 12 '13 at 20:57
  • @n1ckp In the case of a vector, assigning to an index outside of the current size is not allowed. In a map however it is possible to assign to an index which doesn't currently exist. – BlueSilver Feb 28 '14 at 12:37
  • @BlueSilver: that is actually my question, why is it allowed for a map (but not for a vector)? I know how these class works, I just want to know why it was made that way. Please read other comments and answers because I think this was discussed already. – n1ckp Feb 28 '14 at 19:35