Are multiple mutations within initializer lists undefined behavior?
Asked Answered
P

2

22

I am curious about initializer lists and sequence points. I read a while ago that the order of evaluation in initializer lists is left to right. If that is so, then there must be some kind of sequence point between the points of evaluation, am I wrong? So with that said is the following valid code? Is there anything that causes undefined behavior in it?

int i = 0;

struct S {
    S(...) {} 
    operator int() { return i; }
};

int main() {
    i = S{++i, ++i};
}

Any and all responses are appreciated.

Pompidou answered 21/1, 2013 at 16:22 Comment(0)
O
20

Yes, the code is valid and does not have undefined behavior. Expressions in an initizalizer list are evaluated left-to-right and sequenced before the evaluation of the constructor of S. Therefore, your program should consistently assign value 2 to variable i.

Quoting § 8.5.4 of the C++ Standard:

"Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list."

Thus, what happens is:

  1. ++i gets evaluated, yielding i = 1 (first argument of S's constructor);
  2. ++i gets evaluated, yielding i = 2 (second argument of S's constructor);
  3. S's constructor is executed;
  4. S's conversion operator is executed, returning value 2;
  5. value 2 is assigned to i (which already had value 2).

Another relevant paragraph of the Standard is § 1.9/15, which also mentions similar examples that do have undefined behavior:

i = v[i++]; // the behavior is undefined
i = i++ + 1; // the behavior is undefined

However, the same paragraph says:

"Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function."

Since 1) the evaluation of the expressions in the initializer list is sequenced left-to-right, 2) the execution of the constructor of S is sequenced after the evaluation of all expressions in the initializer list, and 3) the assignment to i is sequenced after the execution of the constructor of S (and its conversion operator), the behavior is well-defined.

Oho answered 21/1, 2013 at 16:30 Comment(11)
Are you sure about this? In C++03, i = ++i; (for example) is undefined behavior even though the evaluation order is guaranteed, because i is modified twice between sequence points. I am no C++11 expert -- and I know C+11 ditched the concept of sequence points -- but these examples look awfully similar to my eye.Strath
@Nemo: C++11 still has a concept of sequencing, it's just a bit more complex than the notion of "sequence points" in order to account for multiple threads. However, the answer is missing the more crucial sentence that follows the one quoted: "That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list." So the code is indeed well-defined.Innards
@Strath But isn't there a sequence point in my int() overload? Making the behavior defined?Pompidou
@MikeSeymour: are you sure that part of the sentence is relevant to this? it only explains that expression evaluations are sequenced within an initializer list, but does not seem to refer to Nemo's doubt about left side and right side of operator =. i believe 1.9/15 solves that. am I wrong?Oho
@AndyProwl: That sentence is definitely relevant to whether the evaluation of the initialiser-list is well-defined; each list item depends on the side-effects of the previous, so those must be sequenced as well as the evaluation. There's no problem with the assignment here, since the calls to the constructor and conversion operator both introduce sequence points.Innards
I still think this is undefined behavior because it seems the same as i = ++(++i)Pompidou
@MikeSeymour: i see your point, thank you for clarifying. i still feel that Nemo was referring to the assignment though, which is why i was wondering whether the part of the sentence I omitted was relevant to answer his doubt.Oho
@David: No, it's not the same - there are sequence points between each initialiser clause, and at the function calls for the constructor and conversion operator, meaning that the whole expression i = S{++i,++i} is well sequenced; while in i = ++i, the side-effects are unsequenced with the assignment, giving undefined behaviour.Innards
@Mike: So if f is an ordinary function, then i = f(++i) is defined?Strath
@Nemo: yes, ++i and its side effects are sequenced before the function call, and therefore before the assignment.Innards
@Strath this is also well defined pre C++11 as well, although the evaluation order is unspecified. I created a self answered question covering this pre C++11 since this was a source of contention in another question and figuring this out for myself was enlightening.Cumshaw
B
-3

Yes indeed you have case of undefined behavior.

Below are examples of situations causing undefined behavior:

  • A variable is changed several times within one sequence point. As a canonical example, the i=i++ expression is often cited where assignment of the i variable and its increment are performed at the same time. To learn more about this kind of errors, read the section "sequence points".
  • Using a variable before initializing it. Undefined behavior occurs when trying to use the variable.
  • Memory allocation using the new [] operator and subsequent release using the delete operator. For example: T *p = new T[10]; delete p;. The correct code is: T *p = new T[10]; delete [] p;.

EDITED

Also your code S{++i, ++i}; is not compiled for VS2012. May be you mean S(++i, ++i);? If you use "()" then undefined behavour is present. In other case your source code is incorrect.

Bewley answered 22/1, 2013 at 12:49 Comment(2)
There is no undefined behavior here: the brace initialization syntax is regular C++11, and the fact that VS2012 does not support it is a limitation of that IDE. When brace initialization is used, the expressions are evaluated left-to-right, according to the paragraph of the Standard I quoted in my answer. Secondly, he is not using a variable before initializing it. The variable is global and is initialized to 0. So if it is you who downvoted my answer, you may want to reconsider your decision :-) This answer is not correct for C++11.Oho
C++11 wasn't really fully supported until VS2015. Brace initialization was supported in VS2013 but not VS2012.Ripply

© 2022 - 2024 — McMap. All rights reserved.