Is It Defined Behavior to Reference an Early Member from a Later Member Expression During Aggregate Initialization

Aggregate Initialization of struct with dependent fields

If you liked your conundrum, this is really gonna bake your noodle:

#include <iostream>

struct Foo
{
int bar=0;
int baz=1;
};

const int cool(const Foo& f)
{
std::cout << "Bar: " << f.bar << " Baz: " << f.baz << std::endl;
return f.bar;
}

int main()
{
Foo instance = { 5, cool(instance) };
std::cout << instance.baz << std::endl;
}

What a previous poster correctly quoted: c++ std draft doc

...all value computations and side effects associated with a given element are sequenced before those of any element that follows it in order.

Can I Reference Previous Members of an Initializer List?

So what we have here is aggregate initialization covered in section 8.5.1 of the draft C++ standard and it says:

An aggregate is an array or a class [...]

and:

When an aggregate is initialized by an initializer list, as specified
in 8.5.4, the elements of the initializer list are taken as
initializers for the members of the aggregate, in increasing subscript
or member order. Each member is copy-initialized from the
corresponding initializer-clause [...]

Although it seems reasonable that side effects from initializing each member of the aggregate should be sequenced before the next, since each element in the initializer list is a full expression. The standard does not actually guarantee this we can see this from defect report 1343 which says:

The current wording does not indicate that initialization of a non-class object is a full-expression, but presumably should do so.

and also notes:

Aggregate initialization could also involve more than one full-expression, so the limitation above to “initialization of a non-class object” is not correct.

and we can see from a related std-discussion topic Richard Smith says:

[intro.execution]p10: "A full-expression is an expression that is not
a subexpression of another expression. [...] If a language construct
is defined to produce an implicit call of a function, a use of the
language construct is considered to be an expression for the purposes
of this definition."

Since a braced-init-list is not an expression, and in this case it
does not result in a function call, 5 and s.i are separate
full-expressions. Then:

[intro.execution]p14: "Every value computation and side effect
associated with a full-expression is sequenced before every value
computation and side effect associated with the next full-expression
to be evaluated."

So the only question is, is the side-effect of initializing s.i
"associated with" the evaluation of the full-expression "5"? I think
the only reasonable assumption is that it is: if 5 were initializing a
member of class type, the constructor call would obviously be part of
the full-expression by the definition in [intro.execution]p10, so it
is natural to assume that the same is true for scalar types.

However, I don't think the standard actually explicitly says this
anywhere.

So this is currently not specified by the standard and can not be relied upon, although I would be surprised if an implementation did not treat it the way you expect.

For a simple case like this something similar to this seems a better alternative:

constexpr int value = 13 ;
const int foo[2] = {value, value+42};

Changes In C++17

The proposal P0507R0: Core Issue 1343: Sequencing of non-class initialization clarifies the full-expression point brought up here but does not answer the question about whether the side-effect of initialization is included in the evaluation of the full-expression. So it does not change that this is unspecified.

The relevant changes for this question are in [intro.execution]:

A constituent expression is defined as follows:

(9.1) — The constituent expression of an expression is that expression.

(9.2) — The constituent expressions of a braced-init-list or of a (possibly parenthesized) expression-list are the
constituent expressions of the elements of the respective list.

(9.3) — The constituent expressions of a brace-or-equal-initializer of the form = initializer-clause are the
constituent expressions of the initializer-clause.

[ Example:

struct A { int x; };
struct B { int y; struct A a; };
B b = { 5, { 1+1 } };

The constituent expressions of the initializer used for the initialization of b are 5 and 1+1. —end example ]

and [intro.execution]p12:

A full-expression is

(12.1) — an unevaluated operand (Clause 8),

(12.2) — a constant-expression (8.20),

(12.3) — an init-declarator (Clause 11) or a mem-initializer (15.6.2), including the constituent expressions of the
initializer,

(12.4) — an invocation of a destructor generated at the end of the lifetime of an object other than a temporary
object (15.2), or

(12.5) — an expression that is not a subexpression of another expression and that is not otherwise part of a
full-expression.

So in this case both 13 and foo[0] + 42 are constituent expression which are part of a full-expression. This is a break from the analysis here which posited that they would each be their own full-expressions.

Changes In C++20

The Designated Initialization proposal: P0329 contains the following addition which seems to make this well defined:

Add a new paragraph to 11.6.1 [dcl.init.aggr]:

The initializations of the elements of the aggregate are evaluated in the element order. That is,
all value computations and side effects associated with a given element are sequenced before those of any element that follows it in order.

We can see this is reflected in the latest draft standard.

In a given place in an aggregate initialization list, are values passed into previous places safe to read from the corresponding members?

Yes, because aggregate member initialization is sequenced.

[dcl.init.aggr]/2 has:

When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list
are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each
member is copy-initialized from the corresponding initializer-clause.

[dcl.init.list]/4 has:

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.

The copy-initialization of an aggregate member is certainly a side effect ([intro.execution]/12), and must be "associated with" the corresponding initializer-clause, because that is its full-expression (since an initializer-list is not an expression).

Every up-to-date compiler I have tried (MSVC, Clang, g++) compiles this correctly. It may be possible that some older compilers get it wrong (older versions of g++ were known for getting the sequencing of aggregate initializers wrong).

Is Passing Reference From Child To Parent During Construction UB?

Yes your code is fine.

You can use memory addresses and reference to not yet initialized members in the constructor. What you cannot do is using the value before it has been initialized. This would be undefined behavior:

struct BROKEN {
BROKEN( int* id ) : id(*id) {}
int id; // ^ -------- UB
};

struct B : BROKEN {
B() : BROKEN( &a ) {}
int a;
};


[...] being passed in to the Test constructor, does not yet have an address and is thus Undefined Behaviour

Consider what happens when an object is created. First memory is allocated, then the constructor is called. Hence "does not yet have an address" is not correct.

Initialize rvalue reference member

Your class A is an aggregate type, but B isn't, because it has a user-provided constructor and a private non-static member.

Therefore A a{ std::string{"hello world"} }; is aggregate initialization which does extend the lifetime of the temporary through reference binding to that of a.

On the other hand B is not aggregate initialization. It calls the user-provided constructor, which passes on the reference. Passing on references does not extend the lifetime of the temporary. The std::string object will be destroyed when the constructor of B exits.

Therefore the later use of a has well-defined behavior, while that of b will have undefined behavior.

This holds for all C++ versions since C++11 (before that the program would be obviously ill-formed).

If your compiler is printing garbage for a (after removing b from the program so that it doesn't have undefined behavior anymore), then this is a bug in the compiler.


Regarding edit of question:

There is no way to extend the lifetime of a temporary through binding to a reference member of a non-aggregate class.

Relying on this lifetime extension at all is very dangerous, since you would likely not get any error or warning if you happen to make the class non-aggregate in the future.

If you want the class to always retain the object until its lifetime ends, then just store it by-value instead of by-reference:

class B
{
std::string str;
public:
B(std::string str) : str(std::move(str)) {}
void print() { std::cout << str << '\n'; }
};

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?

rvalue data member initialization: Aggregate initialization vs constructor

Baz is a class with a constructor. Therefore, when you use list initialization, the compiler will look for a constructor to call. That constructor will be passed the members of the braced-init-list, or a std::initializer_list if one matches the members of the list. In either case, the rules of temporary binding to function parameters are in effect ([class.temporary]/6.1):

A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

However, Bar is not a class with a constructor; it is an aggregate. Therefore, when you use list initialization, you (in this case) invoke aggregate initialization. And therefore, the member reference will be bound to the given prvalue directly. The rule for that is ([class.temporary]/6):

The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

Followed by 3 exceptions which do not apply to this case (including 6.1, quoted above).

The lifetime of the reference Bar::foo extends to the end of main. Which doesn't happen until cin.get() returns.


How can I implement the constructor of Baz correctly?

If by "correctly", you mean "like Bar", you cannot. Aggregates get to do things that non-aggregates can't; this is one of them.

It's similar to this:

struct Types { int i; float f };
using Tpl = std::tuple<int, float>;

int &&i1 = Types{1, 1.5}.i;
int &&i2 = std::get<0>(Tpl{1, 1.5});

i2 is a dangling reference to a subobject of a destroyed temporary. i1 is a reference to a subobject of a temporary whose lifetime was extended.

There are some things you just can't do through functions.

Does the standard define list initialization with superfluous braces (e.g. T{{{10}}})?

When you have

Test b{{2}}; 

[dcl.init.list]/3.7 states.

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). [...]

and looking in [over.match] we have [over.match.ctor]/1

When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer.

So we consider the constructors, find

Test(const int a)

and then we use the element {2} as initializer for a which uses [dcl.init.list]/3.9

Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization); if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.

With

Test c{{{3}}};
// and
Test d{{{{4}}}};

we do the same thing. We look at the constructors and find

Test(const int a)

as the only viable one. When we do and try to initialize a, we look to [dcl.init.list]/3.9 again but it doesn't apply here. {{3}} and {{{4}}} aren't initializer lists with a single type E. A braced-init-list doesn't have a type so we have to keep going list in [dcl.init.list]/3. When we do we don't meet anything else that matches until [dcl.init.list]/3.12

Otherwise, the program is ill-formed.



Related Topics



Leave a reply



Submit