首页 > 解决方案 > 必须公开聚合字段构造函数才能在 C++ 中使用聚合初始化吗?

问题描述

请考虑具有带有私有构造函数B的类字段的聚合结构的代码:A

class A { A(int){} friend struct B; };
struct B { A a{1}; };

int main()
{
    B b; //ok everywhere, not aggregate initialization
    //[[maybe_unused]] B x{1}; //error everywhere
    [[maybe_unused]] B y{}; //ok in GCC and Clang, error in MSVC
}

我的问题是关于B. 由于初始化是代表调用代码(main此处的函数)进行的,我希望编译器必须拒绝它,因为A' 的构造函数是私有的。事实上B{1},所有编译器的构造都失败了。

但令我惊讶的B{}是,GCC 和 Clang 都接受了该构造,演示:https ://gcc.godbolt.org/z/7851esv6Y

并且只有 MSVC 以错误拒绝它error C2248: 'A::A': cannot access private member declared in class 'A'

是 GCC 和 Clang 中的错误,还是标准允许他们接受此代码?

标签: c++language-lawyerc++20aggregate-initialization

解决方案


...与聚合结构B...

为了完整起见,让我们首先注意根据[dcl.init.aggr]/1(N4861(2020 年 3 月布拉格后工作草案/C++20 DISB)确实是 C++14 到 C++20 中的聚合)):

聚合是一个数组或一个类 ([class])

  • (1.1)没有用户声明或继承的构造函数([class.ctor]),
  • (1.2) 没有私有或受保护的直接非静态数据成员 ([class.access]),
  • (1.3) 没有虚函数 ([class.virtual]),并且
  • (1.4) 没有虚拟、私有或受保护的基类 ([class.mi])。

而在 C++11 中,由于不违反non-static data members 的大括号或等式初始化器B,而被取消资格作为聚合,这是在 C++14 中删除的要求。

因此,根据[dcl.init.list]/3 B x{1}并且B y{}都是聚合初始化:

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

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

对于前一种情况,根据[dcl.init.aggr]/3B x{1}, 的数据成员a是聚合B显式初始化元素。这意味着,根据[dcl.init.aggr]/4,特别是/4.2,数据成员是从初始化子句复制初始化的,这需要在聚合初始化的上下文中构造一个临时对象,使程序格式错误,因为匹配的构造函数是私有的。AA

B x{1}; // 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 a{1}; };

int main() {
    auto a{A::get()};
    [[maybe_unused]] B x{a}; // OK
}

对于后一种情况,B y{}根据[dcl.init.aggr]/3.3,数据成员a不再B是聚合的显式初始化元素,并且根据[dcl.init.aggr]/5,特别是/5.1

对于非联合聚合,不是显式初始化元素的每个元素都按如下方式初始化:

  • (5.1) 如果元素具有默认成员初始化程序 ([class.mem]),则从该初始化程序初始化该元素。
  • [...]

并且数据成员aB从其默认成员初始化程序初始化的,这意味着A::A(int)不再从无法访问的上下文中访问私有构造函数。


最后是私有析构函数的情况

如果我们添加私有析构函数,A那么所有编译器都会以正确的错误证明它:

[dcl.init.aggr]/8 [强调我的] 管理:

类类型的每个元素的析构函数可能从发生聚合初始化的上下文中调用([class.dtor]) 。[注意:此规定确保可以为完全构造的子对象调用析构函数,以防抛出异常([except.ctor])。——尾注]


推荐阅读