我想创建两个对象,它们之间有相互的成员引用。之后,它可以扩展为例如N个引用对象的闭环,其中N在编译时是已知的。
最初的尝试是使用最简单的结构体A
,它没有任何构造函数,这使得它成为一个聚合体(v
模拟了一些负载):
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() );
不幸的是,它不被编译器接受,因为错误:
accessing uninitialized member 'B::a2'
实际上很奇怪,因为没有真正的读访问,只是记住它的地址。演示:https://gcc.godbolt.org/z/cGzYx1Pea
在A
中加入构造函数后,问题得到了解决,使其不再聚合:
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;
};
现在所有的编译器都接受这个程序,demo: https://gcc.godbolt.org/z/bs17xfxEs
令人惊讶的是,看似相等的程序修改使其有效。在这种情况下,标准中是否真的有一些措辞阻止了聚合的使用?究竟是什么让第二个版本安全且被接受?
B z{ z.a2, z.a1 };
尝试复制构建a1
和a2
,而不是将z.a2
,z.a1
作为第一字段进行聚合初始化1
B z{{z.a2, 0}, {z.a1, 0}};
可以在GCC和Clang中使用。MSVC给出了error C2078: too many initializers
,看起来像一个bug。
1这里,对z
执行直接列表初始化,在这种情况下,它解析为聚合初始化,然后对每个成员执行复制初始化,并且:
[dcl.init.general]/15.6.2
…如果是复制初始化,源类型的cv不限定版本与目标类相同,或者是目标类的派生类,则需要考虑构造函数。
因此,由于初始化式z.a2
、z.a1
与对应的成员具有相同的类型,因此忽略了成员的聚合性,并使用复制构造函数。