Is indexing a new map element and having something that reads it assigned to it undefined behaviour, or just unspecified?
C

2

4

After answering this question, there was a long discussion over whether the code in question was undefined behaviour or not. Here's the code:

std::map<string, size_t> word_count;
word_count["a"] = word_count.count("a") == 0 ? 1 : 2;

First of all, it was well-established that this was at least unspecified. The result differs based on which side of the assignment is evaluated first. In my answer, I followed through each of the four resulting cases, with factors of which side is evaluated first and whether the element exists prior to this.

There was a short form that came up as well:

(x = 0) = (x == 0) ? 1 : 2; //started as
(x = 0) = (y == "a") ? 1 : 2; //changed to

I claimed it was more like this:

(x = 0, x) = (x == 0) ? 1 : 2; //comma sequences x, like [] should

Eventually, I found an example that seemed to work for me:

i = (++i,i++,i); //well-defined per SO:Undefined Behaviour and Sequence Points

Back to the original, I broke it down into relevant function calls to make it easier to follow:

operator=(word_count.operator[]("a"), word_count.count("a") == 0 ? 1 : 2);
   ^       inserts element^                        ^reads same element
   |
assigns to element

If word_count["a"] does not exist, it was argued that it would be assigned to twice without a sequencing in between. I personally didn't see how that could happen if two things I thought were true actually were:

  1. When a side is picked to be evaluated, the whole side has to be evaluated before the other side can start.

  2. Constructs such as word_count["a"] = 1 exhibit well-defined behaviour, even in the case that an element is inserted and then assigned to.

Are these two statements true? Ultimately, is that actually undefined behaviour, and if it is, why does the second statement work (assuming it does)? If the second is false, I believe all the myMap[i]++;s in the world would be ill-formed.

Helpful Link: Undefined behavior and sequence points

Copilot answered 7/4, 2013 at 17:43 Comment(4)
A related question asked in the context of C: #13936404Valuable
@PascalCuoq, Thanks, it seems pretty relevant. The question is whether it holds true for C++ (it almost assuredly does) and whether that extends to creating a new element in the map.Copilot
There seems to be lots of function calls which introduce sequence points everywhere. On the other hand, if the result is still unspecified, what is the practical use of the expression?Fatma
@BoPersson, I made sure to not stray from the question in my answer. I gave a well-defined method of doing it (unless statement 2 happens to actually be false). I was just interested after that long, mind-murdering discussion what it was actually doing.Copilot
S
5

The behavior is unspecified, but not undefined.

Notice, that in the expression:

word_count["a"] = word_count.count("a") == 0 ? 1 : 2;
//              ^

The assignment operator marked with ^ is the built-in assignment operator, because std::map's operator [] returns a size_t&.

Per Paragraph 5.17/1 of the C++11 Standard on the built-in assignment operator(s):

The assignment operator (=) and the compound assignment operators all group right-to-left. [..] 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. With respect to an indeterminately-sequenced function call, the operation of a compound assignment is a single evaluation.

This means that in a built-in assignment such as:

a = b

First the operands are evaluated (in unspecified order), then the assignment is performed, and finally the value computation of the whole assignment expression is performed.

Considering the original expression:

word_count["a"] = word_count.count("a") == 0 ? 1 : 2;
//              ^

Because of the paragraph quoted above, in no case there are two unsequenced assignments to the same object: the assignment marked with ^ will always be sequenced after the assignment performed by operator [] (as part of the evaluation of the left hand side expression) in case the key "a" is not present in the map.

However, the expression will have a different outcome based on which side of the assignment is evaluated first. Thus, the behavior is unspecified, but not undefined.

Safari answered 7/4, 2013 at 18:38 Comment(4)
Thanks, you have no idea how much this puts my mind to ease after all of that.Copilot
@chris: I fell back into doubt again: 1.9/15 "If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined." Here, I am not sure whether the left hand side expression contains a side effect that modifies a scalar value which is read while evaluating the right hand side expression.Safari
I somehow didn't even see that notification until now, but wow, the number of ways thinking about undefined behaviour can give you an aneurysm is astounding. I'm not sure.Copilot
@chris: I very much agree indeedSafari
B
2

It is unspecified, but not undefined.

word_count.operator[]("a") and word_count.count("a") are function calls. Function executions are guaranteed by standard to not interleave - either first is fully sequenced before second or the other way around.

The specific definition can vary by standard, in C++11 relevant clause is in 1.9/15:

Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function.9

9) In other words, function executions do not interleave with each other.

indeterminately sequenced is defined in 1.9/13:

Evaluations A and B are indeterminately sequenced when either A is sequenced before B or B is sequenced before A , but it is unspecified which.

For example, evaluation of:

word_count["a"] = word_count.count("a");

consists of three parts:

  1. execution of word_count.operator[]("a")
  2. execution word_count.count("a")
  3. assignment

Let < mean 'is sequenced before'. Quoted part of the standard guarantees that either 1 < 2 or 2 < 1. Part quoted in @Andy Prowl answer also shows that both 1 < 3 and 2 < 3. So, there are only two options:

  • 1 < 2 < 3
  • 2 < 1 < 3

In both cases everything is fully sequenced and there is no chance for UB.

Bricker answered 7/4, 2013 at 18:30 Comment(2)
@AndyProwl, Lol! I've never seen that happen before :)Copilot
@chris: It happens! :)Safari

© 2022 - 2024 — McMap. All rights reserved.