Mengapa konstruktor default default dihapus untuk kelas union atau union-like?

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

Dalam kode di atas, GCC dan Clang keduanya mengeluh bahwa konstruktor default untuk union Cdidefinisikan sebagai dihapus.

Namun, aturan yang relevan mengatakan bahwa:

Konstruktor default default untuk kelas X didefinisikan sebagai dihapus jika:

  • X adalah gabungan yang memiliki anggota varian dengan konstruktor default non-sepele dan tidak ada anggota varian X yang memiliki penginisialisasi anggota default ,
  • X adalah kelas non-serikat yang memiliki anggota varian M dengan konstruktor default non-sepele dan tidak ada anggota varian dari serikat anonim yang berisi M memiliki penginisialisasi anggota default ,

Perhatikan kata-kata yang ditekankan. Dalam contoh, IIUC, karena anggota varian bmemiliki penginisialisasi anggota default, konstruktor default tidak boleh didefinisikan sebagai dihapus. Mengapa penyusun ini melaporkan kode ini sebagai format yang salah?

Jika mengubah definisi Cmenjadi

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

Kemudian semua kompiler dapat mengkompilasi kode ini. Perilaku tersebut mengisyaratkan bahwa aturan tersebut sebenarnya berarti:

X adalah gabungan yang memiliki anggota varian dengan konstruktor default non-sepele dan tidak ada penginisialisasi anggota default yang disediakan untuk anggota varian

Apakah ini bug compiler atau kata-kata yang tidak jelas dari aturan itu?

Jawaban

6 ecatmur Dec 22 2020 at 16:24

Ini diubah antara C ++ 14 dan C ++ 17, melalui CWG 2084 , yang menambahkan bahasa yang memungkinkan NSDMI pada (setiap) anggota serikat untuk memulihkan konstruktor default default.

Contoh yang menyertai CWG 2084 sedikit berbeda dengan milik Anda:

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

Di sini NSDMI ada pada anggota non-sepele, sedangkan kata-kata yang diadopsi untuk C ++ 17 memungkinkan NSDMI pada anggota mana pun untuk memulihkan konstruktor default yang gagal. Hal ini karena sebagaimana tertulis dalam DR tersebut,

NSDMI pada dasarnya adalah gula sintaksis untuk penginisialisasi mem

Artinya, NSDMI aktif pada int b = 0;dasarnya setara dengan menulis konstruktor dengan mem-inisialisasi dan badan kosong:

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

Selain itu, aturan yang memastikan bahwa paling banyak satu varian anggota serikat memiliki NSDMI agak tersembunyi di sub- klausa class.union.anon :

4 - [...] Paling banyak satu varian anggota serikat dapat memiliki penginisialisasi anggota default.

Anggapan saya adalah karena gcc dan Clang sudah mengizinkan hal di atas (NSDMI pada anggota serikat non-sepele) mereka tidak menyadari bahwa mereka perlu mengubah implementasi mereka untuk dukungan penuh C ++ 17.

Ini telah dibahas pada daftar std-diskusi pada tahun 2016 , dengan contoh yang sangat mirip dengan Anda:

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

Kesimpulannya adalah bahwa clang dan gcc cacat dalam menolak, meskipun pada saat itu terdapat catatan yang menyesatkan, sebagai akibatnya diubah .

Untuk Clang, bugnya adalah https://bugs.llvm.org/show_bug.cgi?id=39686yang mengembalikan kita ke SO pada konstruktor yang didefinisikan secara implisit dihapus karena anggota varian, N3690 / N4140 vs N4659 / N4727 . Saya tidak dapat menemukan bug yang sesuai untuk gcc.

Perhatikan bahwa MSVC dengan benar menerima , dan menginisialisasi cke .b = 0, yang benar per dcl.init.aggr :

5 - [...] Jika agregat adalah gabungan dan daftar penginisialisasi kosong, maka

  • 5.4 - jika anggota varian memiliki penginisialisasi anggota default, anggota tersebut diinisialisasi dari penginisialisasi anggota defaultnya; [...]
3 MichaëlRoy Dec 22 2020 at 17:30

Serikat pekerja adalah hal yang rumit, karena semua anggota berbagi ruang memori yang sama. Saya setuju, kata-kata aturan tidak cukup jelas, karena meninggalkan yang jelas: Mendefinisikan nilai default untuk lebih dari satu anggota serikat adalah perilaku tidak terdefinisi, atau harus menyebabkan kesalahan kompilator.

Pertimbangkan hal-hal berikut ini:

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. 

Ini jelas tidak harus dikompilasi.

Kode ini dapat dikompilasi, karena A tidak memiliki konstruktor default eksplisit.

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) 

Kode ini tidak dapat dikompilasi, karena A :: x memiliki nilai default eksplisit, ini bertabrakan dengan nilai default eplicit untuk U :: b (pun intended).

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.

Kode ini tidak akan dikompilasi baik di gcc, karena alasan yang sama, tetapi akan berfungsi di MSVC (MSVC selalu sedikit kurang ketat daripada gcc, jadi tidak mengherankan):

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.

Adapun di mana kesalahan dilaporkan, baik pada titik deklarasi atau instantiasi, ini bergantung pada kompiler, gcc dan pnidui melaporkan kesalahan pada titik inisialisasi, dan clang akan melaporkannya saat Anda mencoba membuat instance union.

Perhatikan bahwa sangat tidak disarankan untuk memiliki anggota serikat yang tidak kompatibel dengan bit, atau setidaknya sedikit berhubungan. melakukannya akan merusak keamanan tipe, dan merupakan undangan terbuka untuk bug ke dalam program Anda. Jenis punning tidak masalah, tetapi untuk kasus penggunaan lain, kita harus mempertimbangkan untuk menggunakan std :: variant <>.