Is it defined behavior to reference an early member from a later member expression during aggregate initialization?
Asked Answered
I

4

36

Consider the following:

struct mystruct
{
    int i;
    int j;
};

int main(int argc, char* argv[])
{
    mystruct foo{45, foo.i};   

    std::cout << foo.i << ", " << foo.j << std::endl;

    return 0;
}

Note the use of foo.i in the aggregate-initializer list.

g++ 5.2.0 outputs

45, 45

Is this well-defined behavior? Is foo.i in this aggregate-initializer always guaranteed to refer to the being-created structure's i element (and &foo.i would refer to that memory address, for example)?

If I add an explicit constructor to mystruct:

mystruct(int i, int j) : i(i), j(j) { }

Then I get the following warnings:

main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
     a foo{45, foo.i};
                ^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
     cout << foo.i << ", " << foo.j << endl;

The code compiles and the output is:

45, 0

Clearly this does something different, and I'm assuming this is undefined behavior. Is it? If so, why the difference between this and when there was no constructor? And, how can I get the initial behavior (if it was well-defined behavior) with a user-defined constructor?

Induction answered 5/10, 2015 at 3:26 Comment(6)
Great question. As far as I know the list-initialization ensures only that the order of evaluation of arguments is left-to-right and few cases would not invoke UB which otherwise would be, e.g: mystruct x {++i, ++i}; is well-defined though mystruct y(++i, ++i); is not. Nothing more than that. Since your code is not convered by the specification, thus I think it invokes UB.Aerodrome
Looks like UB by omission ("[defns.undefined] Undefined behavior may be expected when this International Standard omits any explicit definition of behavior...") The standard says ([dcl.init.list]/4) that individual initializers in brace-init-list are evaluated in order, but doesn't say how actually applying each initializer to the corresponding member of an aggregate interacts with that order.Bladdernose
@IgorTandetnik The example in [dcl.init.aggr]/p7 would suggest otherwise.Peroneus
@johannes-schaub-litb based on the criteria or closing as a duplicate George Stocker lists here. Basically views, votes and complete answers I think the close should be the other way around.Mycenae
Given the defect report and std-discussion threads that Johannes pointed out I think it would be unwise to rely on the first case working until the defect report is clarified. I updated my answer accordingly. CC @PeroneusMycenae
The answer is different in C++2a, so what about removing the c++11 and c++14 tag to make this question more general?Rabbinate
M
15

Your second case is undefined behavior, you are no longer using aggregate initialization, it is still list initialization but in this case you have a user defined constructor which is being called. In order to pass the second argument to your constructor it needs to evaluate foo.i but it is not initialized yet since you have not yet entered the constructor and therefore you are producing an indeterminate value and producing an indeterminate value is undefined behavior.

We also have section 12.7 Construction and destruction [class.cdtor] which says:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior [...]

So I don't see a way of getting your second example to work like your first example, assuming the first example is indeed valid.

Your first case seems like it should be well defined but I can not find a reference in the draft standard that seems to make that explicit. Perhaps it is defect but otherwise it would be undefined behavior since the standard does not define the behavior. What the standard does tell us is that the initializers are evaluated in order and the side effects are sequenced, from section 8.5.4 [dcl.init.list]:

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. [...]

but we don't have an explicit text saying the members are initialized after each element is evaluated.

MSalters argues that section 1.9 which says:

Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. [...]

combined with:

[...]very 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 [...]

Is sufficient to guarantee each member of the aggregate is initialized as the elements of the initializer list are evaluated. Although this would be not apply prior to C++11 since the order of evaluation of the initializer list was unspecified.

For reference if the standard does not impose a requirement the behavior is undefined from section 1.3.24 which defines undefined behavior:

behavior for which this International Standard imposes no requirements [ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or [...]

Update

Johannes Schaub points out defect report 1343: Sequencing of non-class initialization and std-discussion threads Is aggregate member copy-initialization associated with the corresponding initializer-clause? and Is copy-initialization of an aggregate member associated with the corresponding initializer-clause? which are all relevant.

They basically point out that the first case is currently unspecified, I will quote Richard Smith:

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 although as indicated in several places it looks like current implementations do what we expect, it seems unwise to rely on it until this is officially clarified or the implementations provide a guarantee.

C++20 Update

With the Designated Initialization proposal: P0329 the answer to this question changes for the first case. It contains the following section:

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

We can see this is reflected in the latest draft standard

Mycenae answered 5/10, 2015 at 9:58 Comment(9)
I modified my answer to take in account MSalters comments above.Mycenae
We have a defect report for this open, see open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1343Leucine
Also see groups.google.com/a/isocpp.org/forum/#!searchin/std-discussion/… and groups.google.com/a/isocpp.org/forum/#!searchin/std-discussion/…Leucine
@JohannesSchaub-litb the std-discussion links don't work :-( Also see my comment on the question itself.Mycenae
@JohannesSchaub-litb thank you, I updated my answer accordingly.Mycenae
somehow google managed to make linking to discussion threads a nightmareLeucine
After p0329, the initialization order is now well-defined. The motivation is showed in p0329r0.Rabbinate
@Rabbinate I am aware I have not had a chance to update my answer yet.Mycenae
@Rabbinate UpdatedMycenae
F
12

From [dcl.init.aggr] 8.5.1(2)

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.

emphasis mine

And

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.

Leads me to believe that each member of the class will be initialized in the order they are declared in the initializer-list and since foo.i is initialized before we evaluate it to initialize j this should be defined behavior.

This is also backed up with [intro.execution] 1.9(12)

Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.

emphasis mine

In your second example we are not using aggregate initialization but list initialization. [dcl.init.list] 8.5.4(3) has

List-initialization of an object or reference of type T is defined as follows:
[...]
- Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7).

So now we would call your constructor. When calling the constructor foo.i has not been initialized so we are copying an uninitialized variable which is undefined behavior.

Forefend answered 5/10, 2015 at 4:0 Comment(15)
Although your first conclusion seems reasonable there is no explicit text that says the members are initialized while the list is being evaluated. Although it seems intuitive to think about like a mem-initializers I suppose one could make an argument that leaving it unspecified allows some some optimization opportunities. If it is not explicit it is undefined.Mycenae
@ShafikYaghmour: Per the definition of [3.10], initializing the member is a side effect of the initializer-clause. (In normal english, we'd call it the main effect, but such is Standardese). Since all side effects of an initializer-clause are sequended-before the next initializer-clause, it follows that the actual initialization is sequenced-before as well.Cattycornered
@Cattycornered that is my take on it. a side effect of the initializer-clause is that the member gets initialized.Forefend
@Cattycornered can you give me an exact quote I don't see that in section 3.10Mycenae
@ShafikYaghmour: Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O functionCattycornered
@Cattycornered but when is it considered complete? This defect report seems to indicate aggregate initialization is not considered complete until the end in at least some cases. In at least some cases it would be invalid to access a member until the full initialization was done. So although I see where you are coming from I think there is some under-specification here.Mycenae
@ShafikYaghmour: It's perfectly normal and in fact common to access members inside a constructor, when by definition the initialization of the whole object isn't yet complete. That's not a problem.Cattycornered
@Cattycornered this phrase in particular The first formulation treats aggregate initialization like a constructor call; even POD-type members of an aggregate could not be accessed before the aggregate initialization completed. would seem to forbid the OPs code in the cases outlined in the defect report.Mycenae
@ShafikYaghmour: That's particular to the defect report. Since aggregate initialization isn't using a function (constructor), there is no during aggregate initialization. Access to POD members is therefore either before or after aggregate initialization.Cattycornered
@Cattycornered for what it is worth I modified my answer to include your argument because it is reasonable and I get it.Mycenae
@Cattycornered thanks for pointing out [intro.execution]. I have added that into the answer.Forefend
".. associated with a given initializer-clause": here it is not clear whether the store to the non-class member is considered to be even part of any expression, though (and therefore, whether it is sequenced with the corresponding initializer clause).Leucine
See open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1343 which is essentially this issue.Leucine
Also see groups.google.com/a/isocpp.org/forum/#!searchin/std-discussion/… and groups.google.com/a/isocpp.org/forum/#!searchin/std-discussion/…Leucine
@Cattycornered see the comments by Johannes and my updated answer.Mycenae
S
1

My first idea was UB, but you are fully in the aggregate initialization case. Draft n4296 for C++ 11 specification is explicit in the 8.5.1 Aggregates [dcl.init.aggr] paragraph:

An aggregate is an array or a class with no user-provided constructors , no private or protected non-static data members, no base classes, and no virtual functions

Later:

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

(emphasize mine)

My understanding is that mystruct foo{45, foo.i}; first initializes foo.i with 45, then foo.j with foo.i.

I would not dare to use that in real code anyway, because even if I believe it is defined by standard, I would be afraid that a compiler programmer has thought differently...

Sarene answered 5/10, 2015 at 10:23 Comment(2)
I don't think that quote means what you are claiming ... it is saying that the N-th list item initializes the N-th member of the aggregate, and no more. It doesn't say whether or not the initializers may be evaluated before they are used (although i think there might be another general text about braced initialization that does specify that)Barnebas
@Barnebas indeed, Johannes provided the relevant defect and std-discussion links and I updated my answer accordinhly.Mycenae
F
-2

how can I get the initial behavior (if it was well-defined behavior) with a user-defined constructor?

Passing parameter by reference for that parameter which refers to previously initialized parameter of being constructed object, as follows:

 mystruct(int i, int& j):i(i),j(j)
Freespoken answered 5/10, 2015 at 5:29 Comment(3)
Adding the constructor makes the reference foo.i UB by itself. [class.cdtor]/1: "For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior."Peroneus
@Peroneus does the ctor initializer list count as "before the constructor begins"?Barnebas
The argument has to be evaluated before it can be passed to the ctor, so yes: you're taking a reference to something that doesn't exist yet.Lobworm

© 2022 - 2024 — McMap. All rights reserved.