Member initializers and aggregates

ISO/IEC JTC1 SC22 WG21 N3605 - 2013-03-15

Ville Voutilainen, ville.voutilainen@gmail.com

Abstract

Bjarne Stroustrup and Richard Smith raised an issue about aggregate initialization and member-initializers not working together. This paper proposes to fix the issue by adopting Smith's proposed wording that removes a restriction that aggregates can't have member-initializers.

Introduction

In c++std-core-23029, Bjarne Stroustrup pointed out that aggregate initialization can't be used if a class has member-initializers. He provided the following example:


struct Univ {
    string name;
    int rank;
    string city = "unknown";
};

void t1()
{
    Univ u = {"Columbia",10};
    cout << u.name << ' ' << u.rank << ' ' << u.city << '\n';
}

Ville Voutilainen, Vinny Romano and Jonathan Wakely explained that this is caused by the restrictions in [decl.init.aggr]/1, which says
An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal- initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).
The issue here is the "no brace-or-equal-initializers for non-static data members". Also, [dcl.init.list]/3 specifies that
List-initialization of an object or reference of type T is defined as follows:

- If T is an aggregate, aggregate initialization is performed (8.5.1).

Thus, aggregate initialization can't be used because the class type in question is not an aggregate.

Proposed solution

Johannes Schaub pointed out that this issue had been raised by Richard Smith earlier, and a proposed solution had been presented in [c++std-ext-13002]. Smith proposes to just remove the restriction that prevents aggregates from having member initializers, thus:

Change in 8.5.1 (dcl.init.aggr) paragraph 1:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal- initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).
Change in 8.5.1 paragraph 7:
If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member not explicitly initialized shall be initialized from its brace-or-equal-initializer, if present, or otherwise from an empty initializer list (8.5.4). [ Example: struct S { int a; const char* b; int c; int d = b[a]; }; S ss = { 1, "asdf" }; initializes ss.a with 1, ss.b with "asdf", and ss.c with the value of an expression of the form int{}(), that is, 0, and ss.d with the value of ss.b[ss.a], that is, 's'. -end example ]

Concerns about the solution

Mike Miller voiced some concern whether we should just allow aggregates to have member-initializers, or whether something more generic would be more appropriate:

That said, however, if we adopt Richard's approach, it seems like a straightforward enough change that I'd be willing for CWG to take it. My only question is whether that really is what we want to do. That limits the utility of omitting initializers to trailing members, much like default arguments. Maybe EWG should take a quick pass over it to see if this is sufficient or if we want something more comprehensive, like designated initializers or initializer adapters or some such. (Bjarne rightly warns about a multitude of small, unconnected extensions, which this strikes me as being.)
Miller further clarified that with such a solution, it's possible to provide fewer initializers than there are members, but the omitted ones must be for the members at the end:
It's just like default arguments: you have to declare your non-static data members in order of how likely they are to use the default value. If you have a dozen non-static data members and, for some reason, you only need to specify a non-default value for the 12th one, you still have to give explicit initializers for the other eleven because the association is only by lexical ordering. Designated initializers (or something more C++-like, some sort of adapter, perhaps) would allow you to provide only the non-default initializers.
Daniel Krügler pointed out teachability issues for aggregates:
Aggregates cannot have user-defined constructors and member-initializers are essentially some kind of user-defined constructor (element) (see also Core Defect 886). I'm not against this extension, but it also has implications on what our model of aggregates actually is. After acceptance of this extension I would like to know how to teach what an aggregate is.
Krügler also added the following:
The term "aggregate" is very popular beyond the standard text. People often ask what an aggregate is, because it is (or at least was) a fundamental type characteristic.
Clark Nelson asked whether the proposed fix is as simple as people think:
Personally, I think this is going to be far more than just a bug fix, and I think it's pretty unlikely to make 2014. For example, what does this mean:

struct X { int i, j, k = 42; };

X a[] = { 1, 2, 3, 4, 5, 6 };

Does "a" have two elements or three? Is it actually possible to override the "default" value for k? Either answer is going to disappoint someone.
Stroustrup asked whether the initialization of a above should just be ambiguous. Mike Miller suggested the following:
My perhaps naive expectation was that brace elision would continue to work as it always has and that a non-static data member initializer in an aggregate would be as analogous as possible to one in a class with a constructor: i.e., if there's an explicit initializer for the member, it prevails, and the non-static data member initializer would be used only when the member would otherwise have been default-initialized. I.e., that example is well-formed, giving a two elements, with a[0].k being 3 and a[1].k being 6. That would seem to me to be the least surprising outcome, although it might be not the preferred outcome. Other outcomes could be specified, as you illustrated, using explicit bracing.
Nikolay Ivchenkov provided the following example:
With regard to initialization of members:

    #include 

    struct A
    {
        struct X { int a, b; };
        X x = { 1, 2 };
        int n;
    };

    int main()
    {
        A a = {{10}, 5};
        std::cout << a.x.b << std::endl;
    }
How should a.x.b be initialized here?
Miller explained:
My simple mental model of this says that you only use a non-static data member initializer if the member is not explicitly initialized. In this case, a.x does have an initializer -- "{10}" -- so the non-static data member initializer is ignored, resulting in a.x.b having the value 0.

The solution proposed by this paper

The proposed solution is as proposed by Richard Smith; simply fix the problem by allowing aggregates to have member-initializers, by using the wording Smith provided. Regarding Daniel Krügler's concerns about the concept of an aggregate and its teachability, the author of this paper thinks the benefit of allowing member-initializers in aggregates outweighs such concerns. It would perhaps be possible to keep the definition of an aggregate as is, add a new concept of an extended aggregate that would allow member initializers, and modify the rest of [dcl.init.aggr] to apply to extended aggregates as well as regular aggregates, and modify [dcl.init.list]/3 to apply to aggregates and extended aggregates. Due to an unfortunately busy paper author and a looming pre-mailing deadline, such wording hasn't been created.