Compilation error when using empty list initialization constructor in C++17
In C++14, the definition of aggregate was:
An aggregate is an array or a class (Clause [class]) with no user-provided constructors ([class.ctor]), no private or protected non-static data members (Clause [class.access]), no base classes (Clause [class.derived]), and no virtual functions ([class.virtual]).
Hence, B
is not an aggregate. As a result B{}
is surely not aggregate initialization, and B{}
and B()
end up meaning the same thing. They both just invoke B
's default constructor.
However, in C++17, the definition of aggregate was changed to:
An aggregate is an array or a class with
- no user-provided, explicit, or inherited constructors ([class.ctor]),
- no private or protected non-static data members (Clause [class.access]),
- no virtual functions, and
- no virtual, private, or protected base classes ([class.mi]).
[ Note: Aggregate initialization does not allow accessing protected and private base class' members or constructors. — end note ]
The restriction is no longer on any base classes, but just on virtual/private/protected ones. But B
has a public base class. It is now an aggregate! And C++17 aggregate initialization does allow for initializing base class subobjects.
In particular, B{}
is aggregate initialization where we just don't provide an initializer for any subobject. But the first (and only) subobject is an A
, which we're trying to initialize from {}
(during aggregate initialization, any subobject without an explicit initializer is copy-initialized from {}
), which we can't do because A
's constructor is protected and we are not a friend (see also, the quoted note).
Note that, just for fun, in C++20 the definition of aggregate will change again.
From my understanding of https://en.cppreference.com/w/cpp/language/value_initialization
B{}
does an aggregate_initialization,
and since C++17:
The effects of aggregate initialization are:
- Each direct public base, (since C++17) [..] is copy-initialized from the corresponding clause of the initializer list.
and in our case:
If the number of initializer clauses is less than the number of members and bases (since C++17) or initializer list is completely empty, the remaining members and bases (since C++17) are initialized by their default initializers, if provided in the class definition, and otherwise (since C++14) by empty lists, in accordance with the usual list-initialization rules (which performs value-initialization for non-class types and non-aggregate classes with default constructors, and aggregate initialization for aggregates). If a member of a reference type is one of these remaining members, the program is ill-formed.
So B{/*constructor of A*/}
need to construct base class A, which is protected...
The final draft of C++17 n4659 has a compatibility section which contains the changes with respect to previous versions.
C.4.4 Clause 11: declarators [diff.cpp14.decl]
11.6.1
Change: Definition of an aggregate is extended to apply to user-defined types with base classes.
Rationale: To increase convenience of aggregate initialization.
Effect on original feature: Valid C++ 2014 code may fail to compile or produce different results in this International Standard; initialization from an empty initializer list will perform aggregate initialization instead of invoking a default constructor for the affected types:struct derived; struct base { friend struct derived; private: base(); }; struct derived : base {}; derived d1{}; // Error. The code was well-formed before. derived d2; // still OK
I compiled the above example code with -std=c++14
and it compiled but failed to compile with -std=c++17
.
I believe that could be the reason why the code in the OP fails with B{}
but succeeds with B()
.