为什么为联合或类似联合 class 删除默认的默认构造函数?
Why is the defaulted default constructor deleted for a union or union-like class?
struct A{
A(){}
};
union C{
A a;
int b = 0;
};
int main(){
C c;
}
在上面的代码中,GCC and Clang 都抱怨 union C
的默认构造函数被定义为已删除。
但是,相关规则是这样说的:
A defaulted default constructor for class X is defined as deleted if:
- X is a union that has a variant member with a non-trivial default constructor and no variant member of X has a default member initializer,
- X is a non-union class that has a variant member M with a non-trivial default constructor and no variant member of the anonymous union containing M has a default member initializer,
注意强调的措辞。在示例 IIUC 中,由于变体成员 b
具有默认成员初始值设定项,因此不应将默认的默认构造函数定义为已删除。为什么这些编译器将此代码报告为格式错误?
如果把C
的定义改成
union C{
A a{};
int b;
};
那么所有compilers都可以编译这段代码。该行为暗示该规则实际上意味着:
X is a union that has a variant member with a non-trivial default constructor and no default member initializer is supplied for the variant member
这是编译器错误还是该规则的措辞含糊?
这在 C++14 和 C++17 之间通过 CWG 2084 进行了更改,它添加了允许(任何)联合成员上的 NSDMI 恢复默认构造函数的语言。
CWG 2084 随附的示例与您的略有不同:
struct S {
S();
};
union U {
S s{};
} u;
这里的 NSDMI 在 non-trivial 成员上,而 C++17 采用的措辞允许 any 成员上的 NSDMI 恢复默认的默认构造函数。这是因为,如该 DR 中所写,
An NSDMI is basically syntactic sugar for a mem-initializer
也就是int b = 0;
上的NSDMI基本等价于写一个mem-initializer和空body的构造函数:
C() : b{/*but use copy-initialization*/ 0} {}
顺便说一句,确保 至多 工会的一个变体成员拥有 NSDMI 的规则在某种程度上隐藏在 class.union.anon 的子条款中:
4 - [...] At most one variant member of a union may have a default member initializer.
我的假设是 因为 gcc 和 Clang 已经允许上述(非平凡工会成员上的 NSDMI)他们没有意识到他们需要改变他们的实现完整的 C++17 支持。
这是 discussed on the list std-discussion in 2016,示例与您的非常相似:
struct S {
S();
};
union U {
S s;
int i = 1;
} u;
结论是clang和gcc在reject上有缺陷,虽然当时有误导,结果amended
对于 Clang,错误是 https://bugs.llvm.org/show_bug.cgi?id=39686 which loops us back to SO at Implicitly defined constructor deleted due to variant member, N3690/N4140 vs N4659/N4727。我找不到 gcc 的相应错误。
注意 MSVC correctly accepts, and initializes c
to .b = 0
, which is correct per dcl.init.aggr:
5 - [...] If the aggregate is a union and the initializer list is empty, then
- 5.4 - if any variant member has a default member initializer, that member is initialized from its default member initializer; [...]
联合是一件棘手的事情,因为所有成员共享相同的内存space。我同意,规则的措辞不够清楚,因为它遗漏了显而易见的内容:为一个联合体的多个成员定义默认值是未定义的行为,或者应该导致编译器错误。
考虑以下几点:
union U {
int a = 1;
int b = 0;
};
//...
U u; // what's the value of u.a ? what's the value of u.b ?
assert(u.a != u.b); // knowing that this assert should always fail.
这显然不能编译。
此代码编译通过,因为 A 没有显式默认构造函数。
struct A
{
int x;
};
union U
{
A a; // this is fine, since you did not explicitly defined a
// default constructor for A, the compiler can skip
// initializing a, even though A has an implicit default
// constructor
int b = 0;
};
U u; // note that this means that u.b is valid, while u.a has an
// undefined value. There is nothing that enforces that
// any value contained by a struct A has any meaning when its
// memory content is mapped to an int.
// consider this cast: int val = *reinterpret_cast<int*>(&u.a)
此代码无法编译,因为 A::x 确实有一个明确的默认值,这与 U::b 的明确默认值冲突(双关语)。
struct A
{
int x = 1;
};
union U
{
A a;
int b = 0;
};
// Here the definition of U is equivalent to (on gcc and clang, but not for MSVC, for reasons only known to MS):
union U
{
A a = A{1};
int b = 0;
};
// which is ill-formed.
由于大致相同的原因,此代码无法在 gcc 上编译,但可以在 MSVC 上运行(MSVC 始终不如 gcc 严格,因此这并不奇怪):
struct A
{
A() {}
int x;
};
union U
{
A a;
int b = 0;
};
// Here the definition of U is equivalent to:
union U
{
A a = A{}; // gcc/clang only: you defined an explicit constructor, which MUST be called.
int b = 0;
};
// which is ill-formed.
至于哪里报错,是在声明点还是在实例化点,这个要看编译器,gcc和msvc是在初始化点报错,clang会在你尝试实例化union的时候报错.
请注意,拥有不兼容或至少不相关的联合成员是非常不可取的。这样做会破坏类型安全,并且会公开邀请您的程序出现错误。类型双关是可以的,但对于其他用例,应该考虑使用 std::variant<>.
struct A{
A(){}
};
union C{
A a;
int b = 0;
};
int main(){
C c;
}
在上面的代码中,GCC and Clang 都抱怨 union C
的默认构造函数被定义为已删除。
但是,相关规则是这样说的:
A defaulted default constructor for class X is defined as deleted if:
- X is a union that has a variant member with a non-trivial default constructor and no variant member of X has a default member initializer,
- X is a non-union class that has a variant member M with a non-trivial default constructor and no variant member of the anonymous union containing M has a default member initializer,
注意强调的措辞。在示例 IIUC 中,由于变体成员 b
具有默认成员初始值设定项,因此不应将默认的默认构造函数定义为已删除。为什么这些编译器将此代码报告为格式错误?
如果把C
的定义改成
union C{
A a{};
int b;
};
那么所有compilers都可以编译这段代码。该行为暗示该规则实际上意味着:
X is a union that has a variant member with a non-trivial default constructor and no default member initializer is supplied for the variant member
这是编译器错误还是该规则的措辞含糊?
这在 C++14 和 C++17 之间通过 CWG 2084 进行了更改,它添加了允许(任何)联合成员上的 NSDMI 恢复默认构造函数的语言。
CWG 2084 随附的示例与您的略有不同:
struct S {
S();
};
union U {
S s{};
} u;
这里的 NSDMI 在 non-trivial 成员上,而 C++17 采用的措辞允许 any 成员上的 NSDMI 恢复默认的默认构造函数。这是因为,如该 DR 中所写,
An NSDMI is basically syntactic sugar for a mem-initializer
也就是int b = 0;
上的NSDMI基本等价于写一个mem-initializer和空body的构造函数:
C() : b{/*but use copy-initialization*/ 0} {}
顺便说一句,确保 至多 工会的一个变体成员拥有 NSDMI 的规则在某种程度上隐藏在 class.union.anon 的子条款中:
4 - [...] At most one variant member of a union may have a default member initializer.
我的假设是 因为 gcc 和 Clang 已经允许上述(非平凡工会成员上的 NSDMI)他们没有意识到他们需要改变他们的实现完整的 C++17 支持。
这是 discussed on the list std-discussion in 2016,示例与您的非常相似:
struct S {
S();
};
union U {
S s;
int i = 1;
} u;
结论是clang和gcc在reject上有缺陷,虽然当时有误导,结果amended
对于 Clang,错误是 https://bugs.llvm.org/show_bug.cgi?id=39686 which loops us back to SO at Implicitly defined constructor deleted due to variant member, N3690/N4140 vs N4659/N4727。我找不到 gcc 的相应错误。
注意 MSVC correctly accepts, and initializes c
to .b = 0
, which is correct per dcl.init.aggr:
5 - [...] If the aggregate is a union and the initializer list is empty, then
- 5.4 - if any variant member has a default member initializer, that member is initialized from its default member initializer; [...]
联合是一件棘手的事情,因为所有成员共享相同的内存space。我同意,规则的措辞不够清楚,因为它遗漏了显而易见的内容:为一个联合体的多个成员定义默认值是未定义的行为,或者应该导致编译器错误。
考虑以下几点:
union U {
int a = 1;
int b = 0;
};
//...
U u; // what's the value of u.a ? what's the value of u.b ?
assert(u.a != u.b); // knowing that this assert should always fail.
这显然不能编译。
此代码编译通过,因为 A 没有显式默认构造函数。
struct A
{
int x;
};
union U
{
A a; // this is fine, since you did not explicitly defined a
// default constructor for A, the compiler can skip
// initializing a, even though A has an implicit default
// constructor
int b = 0;
};
U u; // note that this means that u.b is valid, while u.a has an
// undefined value. There is nothing that enforces that
// any value contained by a struct A has any meaning when its
// memory content is mapped to an int.
// consider this cast: int val = *reinterpret_cast<int*>(&u.a)
此代码无法编译,因为 A::x 确实有一个明确的默认值,这与 U::b 的明确默认值冲突(双关语)。
struct A
{
int x = 1;
};
union U
{
A a;
int b = 0;
};
// Here the definition of U is equivalent to (on gcc and clang, but not for MSVC, for reasons only known to MS):
union U
{
A a = A{1};
int b = 0;
};
// which is ill-formed.
由于大致相同的原因,此代码无法在 gcc 上编译,但可以在 MSVC 上运行(MSVC 始终不如 gcc 严格,因此这并不奇怪):
struct A
{
A() {}
int x;
};
union U
{
A a;
int b = 0;
};
// Here the definition of U is equivalent to:
union U
{
A a = A{}; // gcc/clang only: you defined an explicit constructor, which MUST be called.
int b = 0;
};
// which is ill-formed.
至于哪里报错,是在声明点还是在实例化点,这个要看编译器,gcc和msvc是在初始化点报错,clang会在你尝试实例化union的时候报错.
请注意,拥有不兼容或至少不相关的联合成员是非常不可取的。这样做会破坏类型安全,并且会公开邀请您的程序出现错误。类型双关是可以的,但对于其他用例,应该考虑使用 std::variant<>.