Mutual referencing among objects in C++

I want to create two objects with mutual member-references between them. Later it can be extended to e.g. closed loop of N referencing objects, where N is known in compile time.

The initial attempt was with the simplest struct A lacking any constructors, which make it an aggregate (v simulates some payload):

struct A {
    const A & a;
    int v = 0;

struct B {
    A a1, a2;

consteval bool f()
    B z{ z.a2, z.a1 };
    return &z.a1 == &z.a2.a;

static_assert( f() );

Unfortunately it is not accepted by the compilers due to the error:

accessing uninitialized member 'B::a2'

which is actually strange, because no real read access is done, only remembering of its address. Demo:

The problem is solved after adding constructors in A, making it not-aggregate any more:

struct A {
    constexpr A(const A & a_) : a(a_) {}
    constexpr A(const A & a_, int v_) : a(a_), v(v_) {}
    const A & a;
    int v = 0;

Now all compilers accept the program, demo:

It is surprising that seemingly equivalent modification of the program makes it valid. Is it really some wording in the standard preventing the usage of aggregates in this case? And what exactly makes the second version safe and accepted?


B z{ z.a2, z.a1 }; attempts to copy-construct a1 and a2, rather than aggregate-initialize them with z.a2, z.a1 as first fields.1

B z{{z.a2, 0}, {z.a1, 0}}; works in GCC and Clang. MSVC gives error C2078: too many initializers, which looks like a bug.

1 Here, direct-list-initialization is performed for z, which in this case resolves to aggregate initialization, which in turn performs copy-initialization for each member, and:


… if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered.

So, because initializers z.a2, z.a1 have the same type as the corresponding members, the aggregate-ness of the members is ingored, and copy constructors are used.