在下面的程序中,聚合结构B
具有字段a
,该字段本身就是聚合。C 20指定的初始化器可以用来设置它的值而不需要花大括号吗?
struct A { int i; };
struct B { A a; };
int main() {
[[maybe_unused]] B x{1}; //ok everywhere
[[maybe_unused]] B y{.a = {1}}; //ok everywhere
[[maybe_unused]] B z{.a = 1}; //ok in MSVC,Clang; error in GCC
}
MSVC和Clang编译器接受此代码。但GCC发布了一个奇怪的错误:
error: 'A' has no non-static data member named 'a'
演示:https://gcc.godbolt.org/z/65j1sTcPG
这是GCC中的错误,还是标准不允许这样的初始化?
GCC是对的,其他人是错的,因为他们假装指定的初始化列表一直都像等价的非指定初始化列表。
为了理解这里发生了什么(以及为什么编译器不同意),让我们看看你的第一个例子:
B x{1};
由于我们使用大括号,列表初始化的规则开始生效。该列表不是指定的初始值设定项列表,因此3.1失败。3.2失败,因为int
不是B
类型,也不是从B
派生的类型。3.3失败,因为B
不是字符数组。最后是3.4,它将引导我们聚合初始化。
[dcl.init.aggr]/3.2告诉我们,B
的显式初始化元素由B::a
组成。
第4段告诉我们显式初始化的元素是如何初始化的。4.1不适用,因为B
不是联合体。而且。。。4.2不适用,因为无法从
1
复制初始化B::a
。
这似乎不应该工作。幸运的是,第15和第16段存在:
括号可以在初始化列表中省略,如下所示。如果初始化子句列表以左大括号开始,那么接下来的以逗号分隔的初始化子句列表将初始化子集合的元素;初始化子句多于元素是错误的。但是,如果子集合的初始化子句列表不以左大括号开头,则只从列表中获取足够的初始化子句来初始化子集合的元素;任何剩余的初始化子句都被留下来初始化集合的下一个元素当前子集合是一个元素。
使用赋值表达式初始化元素时,会考虑所有隐式类型转换([conv])。如果赋值表达式可以初始化元素,则该元素将被初始化。否则,如果元素本身是子集合,则假定大括号省略,并在初始化子集合的第一个元素时考虑赋值表达式。
也就是说,如果初始值设定项无法通过复制初始化来初始化A
,则大括号省略规则生效。可以从{1}
的初始值设定项列表中初始化A
。因此,它是。
这就是指定的初始化器有问题的地方。指定的初始化列表不是初始化列表。因此,大括号省略段落不适用。
因此,B z{. a=1};
必须失败。
其他编译器没有捕捉到这一点的原因可能如下。他们可能通过去掉指定并在非连续元素之间插入任何默认成员初始化器/值初始化来实现指定的初始化器,然后应用正常的聚合初始化器规则。但这并不完全相同,因为指定的初始化列表不参与大括号省略。
GCC拒绝此代码是正确的:它试图从1
([dcl.init.aggr]/4.2)复制initializez.a
,正如注释所说,这不起作用。然而,GCC所设想的用于生成诊断的大括号省略是无效的:对于指定的初始值设定项列表,它并不存在。