带指示符的派生 class 的聚合初始化

Aggregate initialization of a derived class with designator

cppreference可知:

因为 C++17 派生类型:

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.

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 << "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'
}

熟悉内存模型的肯定会觉得很简单,加个强制类型转换就可以了,反正原来baseclass的数据在数据头部。

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++支持多重继承。如果我们最初的设计思路是通过继承多个结构体来形成一个完整的类型,那我们应该怎么办呢?

更进一步,如果我们的接口对于每个继承的类型都是独立的,那么我们需要为所有基类classes给出一个类型转换方法,并且还要知道数据在类型中的偏移量,如果baseclass的数据没有完全放在class的头部,结果绝对是错误的。

如您所见,您不能既使用指定初始化又使用命名基 class。为了做到这一点,需要扩展语言以支持这种情况。见P2287R1,我还没看完。不幸的是,不会制作 C++23。

直到语言直接支持指定基class(或直接指定基class的成员),如果你想使用指定的初始化器你最好的选择是有基classes 作为成员,正如您清楚记录的那样。