必须公开聚合字段构造函数才能在 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 x1B y 都是聚合初始化:

定义了 T 类型的对象或引用的列表初始化 如下:

[...] (3.4) 否则,如果 T 是聚合,则执行聚合初始化 ([dcl.init.aggr])。

对于前一种情况,B x1B 的数据成员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中出现。只要满足这个规则就可以

聚合函数数据分组

使用 `` 在 C++ 中聚合初始化联合

django的聚合函数和aggregateannotate方法使用

Django 与 Postgresql,列必须出现在 GROUP BY 子句中或在聚合函数中使用

求SQL的聚合函数的定义,特点,注意事项等