聚合字段构造函数必须是 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 ]
请考虑聚合结构 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 ]