ユニオンまたはユニオンのようなクラスのデフォルトのデフォルトコンストラクタが削除されるのはなぜですか?

Dec 22 2020
struct A{
    A(){}
};
union C{
   A a;
   int b = 0;
};
int main(){
    C c;
}

上記のコードでは、GCCとClangの両方が、unionのデフォルトコンストラクターがC削除済みとして定義されていると文句を言っています。

ただし、関連するルールには次のように記載されています。

クラスXのデフォルトのデフォルトコンストラクタは、次の場合に削除済みとして定義されます。

  • Xは、重要なデフォルトコンストラクターを持つバリアントメンバーを持ち、Xのバリアントメンバーにはデフォルトメンバー初期化子を持たないユニオンです
  • Xは、重要なデフォルトコンストラクター持つバリアントメンバーMを持つ非共用体クラスであり、Mを含む匿名ユニオンのバリアントメンバーにはデフォルトメンバー初期化子がありません

強調された表現に注意してください。IIUCの例では、バリアントメンバーbにはデフォルトのメンバー初期化子があるため、デフォルトのデフォルトコンストラクターを削除済みとして定義しないでください。これらのコンパイラがこのコードを不正な形式として報告するのはなぜですか?

の定義をCに変更する場合

union C{
   A a{};
   int b;
};

その後、すべてのコンパイラがこのコードをコンパイルできます。この動作は、ルールが実際に意味することを示唆しています。

Xは、重要なデフォルトコンストラクターを持つバリアントメンバーを持ち、バリアントメンバーにデフォルトメンバー初期化子が提供されていないユニオンです。

これはコンパイラのバグですか、それともそのルールのあいまいな表現ですか?

回答

6 ecatmur Dec 22 2020 at 16:24

これは、を介して、C ++ 14とC ++ 17の間で変化したCWG 2084不履行デフォルトコンストラクタを復元する(任意)組合体上NSDMIを可能にする言語を追加し、。

ただし、CWG 2084に付随する例は、実際の例とは微妙に異なります。

struct S {
  S();
};
union U {
  S s{};
} u;

ここで、NSDMIは重要なメンバーにありますが、C ++ 17に採用された表現により、任意のメンバーのNSDMIがデフォルトのデフォルトコンストラクターを復元できます。これは、そのDRに書かれているように、

NSDMIは、基本的にmem-initializerのシンタックスシュガーです。

つまり、onのNSDMIは、int b = 0;基本的にmem-initializerと空の本体を使用してコンストラクターを作成することと同じです。

C() : b{/*but use copy-initialization*/ 0} {}

余談ですが、共用体の最大で1つのバリアントメンバーがNSDMIを持つことを保証するルールは、class.union.anonの副節にいくらか隠されています。

4-[...]ユニオンの最大1つのバリアントメンバーには、デフォルトのメンバー初期化子があります。

私の推測では、gccとClangはすでに上記(重要なユニオンメンバーのNSDMI)を許可しているため、完全なC ++ 17サポートのために実装を変更する必要があることに気づいていませんでした。

これは、2016年のリストstd-discussionで、あなたと非常によく似た例で議論されました。

struct S {
    S();
};
union U {
    S s;
    int i = 1;
} u;

結論として、clangとgccは拒否に欠陥がありますが、当時は誤解を招くようなメモがあり、結果として修正されました。

Clangの場合、バグは https://bugs.llvm.org/show_bug.cgi?id=39686これは、バリアントメンバー、N3690 / N4140とN4659 / N4727が原因で削除された、暗黙的に定義されたコンストラクターでSOにループバックします。gccに対応するバグが見つかりません。

MSVCは、を正しく受け入れ、に初期化cすることに注意してください.b = 0。これは、dcl.init.aggrごとに正しいです。

5-[...]集合体が和集合であり、初期化子リストが空の場合、

  • 5.4-バリアントメンバーにデフォルトのメンバー初期化子がある場合、そのメンバーはデフォルトのメンバー初期化子から初期化されます。[...]
3 MichaëlRoy Dec 22 2020 at 17:30

すべてのメンバーが同じメモリスペースを共有するため、ユニオンは注意が必要です。ユニオンの複数のメンバーにデフォルト値を定義することは未定義の動作であるか、コンパイラエラーにつながる可能性があるため、ルールの表現が十分に明確ではないことに同意します。

次のことを考慮してください。

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がエラーを報告します。

ビット互換ではない、または少なくともビット関連性のあるユニオンのメンバーを持つことは非常に推奨されないことに注意してください。そうすることは型安全性を破壊し、あなたのプログラムへのバグへの公然の誘いです。型のパンニングは問題ありませんが、他のユースケースでは、std :: Variant <>の使用を検討する必要があります。