Are Multiple Mutations Within Initializer Lists Undefined Behavior

Are multiple mutations within initializer lists undefined behavior?

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.

Are multiple mutations of the same variable within initializer lists undefined behavior pre C++11

The code is not undefined pre-C++11 but the evaluation order is unspecified. If we look at the draft standard section 1.9 Program execution paragraph 12 says:

A full-expression is an expression that is not a subexpression of another expression. [...]

and paragraph 15 says:

There is a sequence point at the completion of evaluation of each full-expression12).

then the question is whether count++, count++ is a full expression and each count++ a sub-expression or is each count++ it's own full expression and therefore there is sequence point after each one? if we look at the grammar for this initialization from section 8.5 Initializers:

initializer-clause:
assignment-expression
{ initializer-list ,opt }
{ }
initializer-list:
initializer-clause
initializer-list , initializer-clause

the only expression we have is an assignment-expression and the , separating the components is part of the initializer-list and and not part of an expression and therefore each count++ is a full expression and there is a sequence point after each one.

This interpretation is confirmed by the following gcc bug report, which has very similar code to mine(I came up with my example way before I found this bug report):

int count = 23;
int foo[] = { count++, count++, count++ };

which ends up as defect report 430, which I will quote:

[...]I believe the standard is clear that each initializer expression in the above is a full-expression (1.9 [intro.execution]/12-13; see also issue 392) and therefore there is a sequence point after each expression (1.9 [intro.execution]/16). I agree that the standard does not seem to dictate the order in which the expressions are evaluated, and perhaps it should. Does anyone know of a compiler that would not evaluate the expressions left to right?

Incrementing a variable used twice in an initializer list - undefined behavior?

C

In C (not necessarily the same answer as for C++), there are no sequence points associated with the components of an initializer list.

The C11 standard, ISO/IEC 9899:2011, says in section §6.7.9 Initialization:

¶19 The initialization shall occur in initializer list order, each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject; 151)

151) Any initializer for the subobject which is overridden and so not used to initialize that subobject might not be evaluated at all.

That sounds promising, but…

¶23 The evaluations of the initialization list expressions are indeterminately sequenced with respect to one another and thus the order in which any side effects occur is unspecified.152)

152) In particular, the evaluation order need not be the same as the order of subobject initialization.

So, (in C) the order of evaluation is indeterminately sequenced, and you cannot rely on when the increments occur (or, in extreme cases not illustrated by the code in the question, whether the increments occur).

In C99 (ISO/IEC 9899:1999), the section number is §6.7.8, but paragraphs 19 and 23 have essentially the same content, except that the footnote numbers are different.

In C90 (ISO/IEC 9899:1990), the issue is not addressed explicitly.

C++

Judging from songyuanyao's answer, the rules in C++11 (and later) are different from those in C11. This sort of thing emphasizes that the languages C and C++ are different and makes writing comprehensive answers to questions tagged with both languages extremely difficult.

Closely related questions

There are at least two other questions related to side-effects (such as ++) in contexts other than initializers. They both should be read too. The second, in particular, is of interest to C++ users; the first is tagged C and not C++ and so is of most relevance to those interested in C.

  • Why are these constructs (using ++) undefined behaviour?

  • Undefined behaviour and sequence points

Both were pointed out by πάντα ῥεῖ in the comments.

Order of evaluation in initializer_list c++11

This is one interesting corner of the C++ standard where execution order is well defined. Section 8.5.4 [dcl.init.list], paragraph 4:

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.

So in the initializer list, the function calls are evaluated left-to-right.

Why is this undefined behaviour?

Update 2

So after some research I realized this was actually well defined although the evaluation order is unspecified. It was a pretty interesting putting the pieces together and although there is a more general question covering this for the C++11 case there was not a general question covering the pre C++11 case so I ended up creating a self answer question, Are multiple mutations of the same variable within initializer lists undefined behavior pre C++11 that covers all the details.

Basically, the instinct when seeing makeX(count++), makeX(count++) is to see the whole thing as a full-expression but it is not and therefore each intializer has a sequence point.

Update

As James points out it may not be undefined pre-C++11, which would seem to rely on interpreting the initialization of each element as a full expression but it is not clear you can definitely make that claim.

Original

Pre-C++11 it is undefined behavior to modify a variable more than once within a sequence point, we can see that by looking at the relevant section in an older draft standard would be section 5 Expressions paragraph 4 which says (emphasis mine):

[...]Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.

In the C++11 draft standard this changes and to the following wording from section 1.9 Program execution paragraph 15 says (emphasis mine):

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. [...] 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.

and we can see that for initializer lists from section 8.5.4 List-initialization paragraph 4 says:

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.

std::initializer_list and order of evaluation of the elements

According to C++11 § 8.5.4 [dcl.init.list] paragraph 4:

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

As far as I know GCC 4.8.1 has a bug relative to evaluation of initializers. I described it here

http://cpp.forum24.ru/?1-3-0-00000063-000-0-0-1378892425

Though the text is written in Russion but it can be simply translated in English by using for example google translate.

Interdependent initialization with commas?

The correct answer is that

int x = 42, y = x;

and

int x = 42;
int y = x;

are usually equivalent (not strictly).


Considering the standard § 8 Declarators [dcl.decl]:

3 Each init-declarator in a declaration is analyzed separately as if it was in a declaration by itself.

and in the footnote [100] further explains:

A declaration with several declarators is usually equivalent to the corresponding sequence of declarations each with a single
declarator. That is

T D1, D2, ... Dn;

is usually equivalent to

T D1; T D2; ... T Dn;

where T is a decl-specifier-seq and each Di is an init-declarator.

  • The above guarantees that x = 42 and y = x will be evaluated separately. However, as @Praetorian correctly pointed out in the comments, footnotes are not normative.

  • This means that the order of evaluation is not well defined and an implementer could as well implement the evaluation of the declarations in the reverse order (i.e,. T Dn; ...T D2; T D1;).

  • One might argue that the comma operator is guaranteed left to right evaluation. However, this not the case. According to the K & R [K & R II, 3.6 p.63], that also applies to C++:

The commas that separate function arguments, variables in declarations, etc., are not comma operators, and do not guarantee left to right evaluation.

Why use variadic arguments now when initializer lists are available?

If by variadic arguments you mean the ellipses (as in void foo(...)), then those are made more or less obsolete by variadic templates rather than by initializer lists - there still could be some use cases for the ellipses when working with SFINAE to implement (for instance) type traits, or for C compatibility, but I will talk about ordinary use cases here.

Variadic templates, in fact, allow different types for the argument pack (in fact, any type), while the values of an initializer lists must be convertible to the underlying type of the initalizer list (and narrowing conversions are not allowed):

#include <utility>

template<typename... Ts>
void foo(Ts...) { }

template<typename T>
void bar(std::initializer_list<T>) { }

int main()
{
foo("Hello World!", 3.14, 42); // OK
bar({"Hello World!", 3.14, 42}); // ERROR! Cannot deduce T
}

Because of this, initializer lists are less often used when type deduction is required, unless the type of the arguments is indeed meant to be homogenous. Variadic templates, on the other hand, provide a type-safe version of the ellipses variadic argument list.

Also, invoking a function that takes an initializer list requires enclosing the arguments in a pair of braces, which is not the case for a function taking a variadic argument pack.

Finally (well, there are other differences, but these are the ones more relevant to your question), values in an initializer lists are const objects. Per Paragraph 18.9/1 of the C++11 Standard:

An object of type initializer_list<E> provides access to an array of objects of type const E. [...] Copying an initializer list does
not copy the underlying elements. [...]

This means that although non-copyable types can be moved into an initializer lists, they cannot be moved out of it. This limitation may or may not meet a program's requirement, but generally makes initializer lists a limiting choice for holding non-copyable types.

More generally, anyway, when using an object as an element of an initializer list, we will either make a copy of it (if it is an lvalue) or move away from it (if it is an rvalue):

#include <utility>
#include <iostream>

struct X
{
X() { }
X(X const &x) { std::cout << "X(const&)" << std::endl; }
X(X&&) { std::cout << "X(X&&)" << std::endl; }
};

void foo(std::initializer_list<X> const& l) { }

int main()
{
X x, y, z, w;
foo({x, y, z, std::move(w)}); // Will print "X(X const&)" three times
// and "X(X&&)" once
}

In other words, initializer lists cannot be used to pass arguments by reference (*), let alone performing perfect forwarding:

template<typename... Ts>
void bar(Ts&&... args)
{
std::cout << "bar(Ts&&...)" << std::endl;
// Possibly do perfect forwarding here and pass the
// arguments to another function...
}

int main()
{
X x, y, z, w;
bar(x, y, z, std::move(w)); // Will only print "bar(Ts&&...)"
}

(*) It must be noted, however, that initializer lists (unlike all other containers of the C++ Standard Library) do have reference semantics, so although a copy/move of the elements is performed when inserting elements into an initializer list, copying the initializer list itself won't cause any copy/move of the contained objects (as mentioned in the paragraph of the Standard quoted above):

int main()
{
X x, y, z, w;
auto l1 = {x, y, z, std::move(w)}; // Will print "X(X const&)" three times
// and "X(X&&)" once

auto l2 = l1; // Will print nothing
}


Related Topics



Leave a reply



Submit