为什么为联合或类似联合 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<>.