聚合字段构造函数必须是 public 才能在 C++ 中使用聚合初始化?

Must aggregate field constructor be public to use aggregate initialization in C++?

请考虑聚合结构 B 的代码具有 class 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 的错误,还是标准允许他们接受这段代码?

在我看来,GCC 和 CLANG 的行为是正确的,但 MVSC 不是,原因如下:

  • 正如您已经提到的,结构 B 是一个聚合,因此在对 B 使用列表初始化时会发生聚合初始化 (list initialization
  • 由于初始化列表为空,因此使用默认成员初始化 (aggregate initialization
  • 因为 B 是 A 的朋友,允许使用 A 的私有构造函数进行默认成员初始化

只是为了你不清楚(至少我不清楚)。 B x{1};行导致编译器错误,因为编译器在执行B的成员a的复制初始化之前,试图找到一种方法将初始化列表中的整数1转换为A的实例。但是没有在那个地方执行该转换的方法,因为 A 的构造函数是私有的。这就是你得到编译器错误的原因

... with aggregate struct B ...

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

An aggregate is an array or a class ([class]) with

  • (1.1) no user-declared or inherited constructors ([class.ctor]),
  • (1.2) no private or protected direct non-static data members ([class.access]),
  • (1.3) no virtual functions ([class.virtual]), and
  • (1.4) no virtual, private, or protected base classes ([class.mi]).

而在 C++11 中,B 由于违反了 非静态数据成员的无大括号或相等初始化器 而被取消聚合资格,在 C++14 中删除的要求。

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

List-initialization of an object or reference of type T is defined as follows:

  • [...]
  • (3.4) Otherwise, if T is an aggregate, aggregate initialization is performed ([dcl.init.aggr]).

对于前一种情况,B x{1}B 的数据成员 a 是聚合的 显式初始化元素 ,如根据 [dcl.init.aggr]/3. This means, as per [dcl.init.aggr]/4, particularly /4.2,数据成员是来自初始化子句的 copy-initialized,这需要在聚合初始化,使程序格式错误,因为 A 的匹配构造函数是私有的。

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, the data member a of B is no longer an explicitly initialized element of the aggregate, and as per [dcl.init.aggr]/5, particularly /5.1

For a non-union aggregate, each element that is not an explicitly initialized element is initialized as follows:

  • (5.1) If the element has a default member initializer ([class.mem]), the element is initialized from that initializer.
  • [...]

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


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

If we add private destructor to A then all compilers demonstrate it with the correct error:

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

The destructor for each element of class type is potentially invoked ([class.dtor]) from the context where the aggregate initialization occurs. [ Note: This provision ensures that destructors can be called for fully-constructed subobjects in case an exception is thrown ([except.ctor]). — end note ]