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

风流意气都作罢 提交于 2019-12-17 10:44:30

问题


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?


回答1:


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




回答2:


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.




回答3:


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




回答4:


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)


来源:https://stackoverflow.com/questions/32940847/is-it-defined-behavior-to-reference-an-early-member-from-a-later-member-expressi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!