The definition of aggregate changed since C++17.
Before C++17
no base classes
Since C++17
no
virtual, private, or protected (since C++17)base classes
That means, for B and D, they’re not aggregate type before C++17, then for B{} and D{}, value-initialization will be performed, then the defaulted default constructor will be called; which is fine, because the protected constructor of base class could be called by derived class’s constructor.
Since C++17, B and D become aggregate type (because they have only public base class, and note that for class D, the explicitly defaulted default constructor is allowed for aggregate type since C++11), then for B{} and D{}, aggregate-initialization will be performed,
Each
direct public base, (since C++17)array element, or non-static class member, in order of array subscript/appearance in the class definition, is copy-initialized from the corresponding clause of the initializer list.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 membersand bases (since C++17)are initializedby 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.
That means the base class subobject will be value-initialized directly, the constructor of B and D are bypassed; but the default constructor of A is protected, then the code fails. (Note that A is not aggregate type because it has a user-provided constructor.)
BTW: C (with a user-provided constructor) is not an aggregate type before and after C++17, so it’s fine for both cases.