Reputation: 161
From cppreference we can know:
Since C++17
for derived types:
If the initializer clause is a nested braced-init-list (which is not an expression), the corresponding array element/class member /public base (since C++17) is list-initialized from that clause: aggregate initialization is recursive.
Since C++20
for aggregate types:
Using .designator = arg
to specify initialized members is no longer an extension to C, but is supported by the C++ standard.
struct A
{
int a;
int b;
};
struct B : A
{
int c;
};
void test()
{
A a{.a = 1, .b = 42}; // a.a = 1 && a.b = 42
B b{{.a = 1, .b = 2}, 42}; // b.a = 1 && b.b = 2 && b.c = 42
}
Very simple and intuitive, right?
But let's complicate things a bit.
constexpr int a_very_very_special_value = 42;
struct A
{
int a;
int b;
};
struct B : A
{
int c = a_very_very_special_value;
int d;
};
void test()
{
A a{.a = 1, .b = 42}; // a.a = 1 && a.b = 42
B b{{.a = 1, .b = 2}, .d = 9}; // Ouch! Mixture of designated and non-designated initializers in the same initializer list is a C99 extension.
}
The above situation should be very common, we want a member data to keep its default value, but the standard requires us that .designator = arg
must be after all parameters without .designator
, but, this is impossible, how do we give our base a name? Like .base_type_name = {.a = 1, .b = 2}
?
One might think that things like inheritance are totally unnecessary, we can do it like this.
struct A
{
int a;
int b;
};
struct B
{
A base;
int c = a_very_very_special_value;
int d;
};
void test()
{
A a{.a = 1, .b = 42}; // a.a = 1 && a.b = 42
B b{.base = {.a = 1, .b = 2}, .d = 9}; // b.base.a = 1 && b.base.b = 2 && b.c = a_very_very_special_value && b.d = 9
}
Great, what else is unsatisfactory? Unless we have an interface like this.
void test(A &a)
{
if (a.a + a.b > 42) {
std::cout << "42\n";
}
else {
std::cout << "0\n";
}
}
void test()
{
A a{.a = 1, .b = 42};
B b{.base = {.a = 1, .b = 2}, .d = 9};
test(a);
test(b); // No matching function for call to 'test'
}
Those who are familiar with the memory model will definitely think that it is very simple, just add a forced type cast, anyway, the data of the original base class is in the head of the data.
void test()
{
A a{.a = 1, .b = 42};
B b{.base = {.a = 1, .b = 2}, .d = 9};
test(a);
// test(b);
test((A&)b);
}
However, one very important thing is that C++ supports multiple inheritance. If our original design idea is to form a complete type by inheriting multiple structures, what should we do?
Further, if our interface is separate for each inherited type, then we need to give a type cast method for all base classes, and also know the offset of the data in the type, if the data from the base class is not completely placed at the head of the class, the result will absolutely be wrong.
Upvotes: 4
Views: 1131
Reputation: 303487
As you've discovered, you can't use both designated initialization and also name the base class. In order to do that, the language needs to be extended to support that case. See P2287R1, which I have not finished yet. Unfortunately, won't make C++23.
Until the language directly supports designated the base class (or directly designated members of the base class), if you want to use designated initializers your best bet is to have the base classes as members, as you've clearly documented.
Upvotes: 5