必须公开聚合字段构造函数才能在 C++ 中使用聚合初始化吗?
Posted
技术标签:
【中文标题】必须公开聚合字段构造函数才能在 C++ 中使用聚合初始化吗?【英文标题】:Must aggregate field constructor be public to use aggregate initialization in C++? 【发布时间】:2021-10-12 09:30:37 【问题描述】:请考虑具有聚合结构B
的代码具有类A
的字段和私有构造函数:
class A A(int) friend struct B; ;
struct B A a1; ;
int main()
B b; //ok everywhere, not aggregate initialization
//[[maybe_unused]] B x1; //error everywhere
[[maybe_unused]] B y; //ok in GCC and Clang, error in MSVC
我的问题是关于 B
的聚合初始化。由于初始化是代表调用代码进行的(此处为main
函数),我预计编译器必须拒绝它,因为A
的构造函数是私有的。事实上,B1
的构造在所有编译器中都失败了。
但令我惊讶的是,构造 B
被 GCC 和 Clang 接受,演示:https://gcc.godbolt.org/z/7851esv6Y
只有 MSVC 拒绝它并返回错误 error C2248: 'A::A': cannot access private member declared in class 'A'
。
这是 GCC 和 Clang 中的错误,还是标准允许他们接受此代码?
【问题讨论】:
您是否认为this 一定会因为同样的原因而失败(“默认参数的初始化代表调用代码”)? @n.1.8e9-where's-my-sharem。 ,谢谢,很好,在您的示例中,所有 3 个编译器至少都持有相同的观点。并且在聚合初始值设定项的情况下,它需要有公共析构函数,因此至少从对称的角度来看,需要公共构造函数。但让我们看看标准是怎么说的。 好吧,他们都这样做了,可能是因为标准规定了他们应该做什么。 “在默认参数出现的地方查找默认参数中的名称,并检查语义约束。” “在声明时检查访问是否有默认参数 ([dcl.fct.default]),而不是在默认参数的任何使用点。”我猜想默认成员初始化器也是如此。如果标准没有明确要求,这是一个应该修复的疏忽。其他任何事情都会不一致和令人惊讶。 【参考方案1】:...带有聚合结构
B
...
为了完整起见,让我们首先注意 B
确实是 C++14 到 C++20 中的聚合,根据 [dcl.init.aggr]/1(N4861(2020 年 3 月布拉格后工作草案/C+ +20 DIS)):
聚合是一个数组或一个类([class])
(1.1) 没有用户声明或继承的构造函数 ([class.ctor]), (1.2) 没有私有或受保护的直接非静态数据成员 ([class.access]), (1.3) 没有虚函数 ([class.virtual]),并且 (1.4) 没有虚拟、私有或受保护的基类 ([class.mi])。
而在 C++11 中,B
不符合聚合条件,因为它违反了非静态数据成员没有大括号或相等初始化器,该要求已在 C 中删除++14.
因此,根据[dcl.init.list]/3 B x1
和B y
都是聚合初始化:
定义了
[...] (3.4) 否则,如果T
类型的对象或引用的列表初始化 如下:T
是聚合,则执行聚合初始化 ([dcl.init.aggr])。
对于前一种情况,B x1
,B
的数据成员a
是聚合的显式初始化元素,根据[dcl.init.aggr]/3。这意味着,根据[dcl.init.aggr]/4,尤其是/4.2,数据成员是从初始化子句复制初始化的,这需要在上下文中构造一个临时的A
对象聚合初始化,使程序格式错误,因为A
的匹配构造函数是私有的。
B x1; // needs A::A(int) to create an A temporary
// that in turn will be used to copy-initialize
// the data member a of B.
如果我们改为在初始化子句中使用A
对象,则无需在聚合初始化的上下文中访问A
的私有构造函数,并且程序是良构的。
class A
public:
static A get() return 42;
private:
A(int)
friend struct B;
;
struct B A a1; ;
int main()
auto aA::get();
[[maybe_unused]] B xa; // OK
对于后一种情况,B y
,根据[dcl.init.aggr]/3.3,B
的数据成员a
不再是聚合的显式初始化元素,并且根据[dcl.init.aggr]/5,尤其是/5.1
对于非联合聚合,每个不是显式 初始化元素的初始化如下:
(5.1) 如果元素具有默认成员初始化程序 ([class.mem]),则从该初始化程序初始化元素。 [...]
并且B
的数据成员a
是从其默认成员初始化程序初始化的,这意味着不再从无法访问的上下文中访问私有构造函数A::A(int)
。
最后是私有析构函数的情况
如果我们将私有析构函数添加到
A
,那么所有编译器都会显示正确的错误:
受[dcl.init.aggr]/8[强调我的]管理:
类类型的每个元素的析构函数可能被调用 ([class.dtor]) 从发生聚合初始化的上下文中。 [注意:此规定确保如果抛出异常([except.ctor]),可以为完全构造的子对象调用析构函数。 ——尾注]
【讨论】:
【参考方案2】:在我看来,GCC 和 CLANG 的行为是正确的,但 MVSC 却不是,原因如下:
正如您已经提到的,struct B 是一个聚合,因此在对 B 使用列表初始化时会发生聚合初始化 (list initialization 由于初始化列表为空,所以使用默认成员初始化(aggregate initialization 由于 B 是 A 的朋友,因此允许使用 A 的私有构造函数进行默认成员初始化只是为了你不清楚的情况(至少我不清楚)。 B x1;
行导致编译器错误,因为编译器在执行 B 的成员 a 的复制初始化之前,试图找到一种方法将初始化器列表中的整数 1 转换为 A 的实例。但是没有办法在那个地方执行那个转换,因为 A 的构造函数是私有的。这就是你得到那个编译器错误的原因
【讨论】:
谢谢,但我认为在B
行中发生了聚合初始化。如果我们将私有析构函数添加到A
,那么所有编译器都会显示正确的错误:gcc.godbolt.org/z/3vKTe5n7c
是的,发生了聚合初始化,但是在初始化列表中没有初始化子句的成员(成员 a 就是这种情况)由它们的默认成员初始化器(即A1 代表成员 a)。以上是关于必须公开聚合字段构造函数才能在 C++ 中使用聚合初始化吗?的主要内容,如果未能解决你的问题,请参考以下文章
帮我解释下:所有select的字段,除聚合函数中的字段,都必须在group by中出现。只要满足这个规则就可以
django的聚合函数和aggregateannotate方法使用