Inserting its own size to std::map

Viewed 137

I just encountered a weird bug in my code (C++14) caused by the unexpected (for me at least) behavior of std::map. Here's a simple example demonstrating the behavior:

#include <iostream>
#include <map>

int main()
{
    std::map<int, int> m;
    for(int i = 0; i < 3; ++i) {
        m[m.size()] = m.size();
    }
    for(const std::pair<int, int>& e : m) {
        std::cout << e.first << " " << e.second << std::endl;
    }
    
    return 0;
}

This prints:

0 1                                                                                                                                                                            
1 2                                                                                                                                                                            
2 3

I was expecting:

0 0
1 1
2 2

What is going on here? The map first adds a new element with first set and only then (when the map's size has already increased) sets second? I don't quite understand why this would make sense. Or is there some other explanation? Thanks!

2 Answers

There is a few thing that happens in the expression

m[m.size()] = m.size();

First, m[m.size()] and = m.size() need to be evaluated. In C++14, that evaluation order is indeterminately sequenced. m[m.size()] might happen first, or it might happen second. If it happens first, then you see the results you received. If it happens second, then you get the output that you expected.

If you want

0 0
1 1
2 2

Then you need to guarantee that ordering yourself. You can use map::insert() to do just that:

m.insert(m.size(), m.size());

Starting in C++17, this is no longer the case. The standard was changed to say:

The assignment operator (=) and the compound assignment operators all group right-to-left. All require a modifiable lvalue as their left operand; their result is an lvalue referring to the left operand. The result in all cases is a bit-field if the left operand is a bit-field. In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression. The right operand is sequenced before the left operand. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.

It now guarantees that = m.size() happens before m[m.size()], and you get the order that you expect.

Let's write down the loop code in a more elaborate way:

for(int i = 0; i < 3; ++i) {
    int& temp = m.operator[](m.size());
    temp = m.size();
}

Note that calling operator[] already does the insertion of an default initialized element before you assign a value to it. Therefore, the map has already grown by the time the right hand side of the assignment is evaluated.

To fix the issue, make sure the size is determined before you use operator[]:

for(int i = 0; i < 3; ++i) {
    size_t v = m.size();
    m[v] = v;
}
Related