具有指示符的派生类的聚合初始化



从cppreference我们可以知道:

由于派生类型的C++17

如果初始值设定项子句是嵌套的支撑init列表(它不是表达式(,则相应的数组元素/类成员/公共基(由于C++17(将从该子句列表初始化:聚合初始化是递归的。

由于C++20用于聚合类型:

使用.designator = arg指定初始化的成员不再是C的扩展,而是受C++标准的支持。

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
}

非常简单直观,对吧?

但让我们把事情复杂一点。

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

上面的情况应该很常见,我们希望成员数据保持其默认值,但标准要求.designator = arg必须在没有.designator的所有参数之后,但是,这是不可能的,我们如何给我们的基命名?像.base_type_name = {.a = 1, .b = 2}

有人可能会认为像继承这样的事情是完全没有必要的,我们可以这样做。

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
}

太好了,还有什么不令人满意的?除非我们有这样的界面。

void test(A &a)
{
if (a.a + a.b > 42) {
std::cout << "42n";
}
else {
std::cout << "0n";
}
}
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'
}

熟悉内存模型的人肯定会认为它很简单,只需添加一个强制类型转换,无论如何,原始基类的数据都在数据的头部。

void test()
{
A a{.a = 1, .b = 42};
B b{.base = {.a = 1, .b = 2}, .d = 9};
test(a);
//  test(b);
test((A&)b);
}

但是,有一点非常重要,那就是C++支持多重继承。如果我们最初的设计理念是通过继承多个结构来形成一个完整的类型,我们该怎么办?

此外,如果我们的接口对于每个继承的类型都是单独的,那么我们需要为所有基类提供一个类型转换方法,并且还需要知道数据在类型中的偏移量,如果基类中的数据没有完全放在类的头上,结果将是绝对错误的。

正如您所发现的,您不能同时使用指定的初始化和基类的名称。为了做到这一点,语言需要扩展以支持这种情况。参见P2287R1,我还没有完成。不幸的是,不会生成C++23。

在该语言直接支持指定的基类(或基类的直接指定成员(之前,如果您想使用指定的初始化器,您最好的选择是将基类作为成员,正如您已经清楚地记录的那样。