Dlaczego domyślny konstruktor domyślny jest usuwany z klasy unii lub unii?

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

W powyższym kodzie GCC i Clang narzekają, że domyślny konstruktor dla union Cjest zdefiniowany jako usunięty.

Jednak odpowiednia zasada mówi, że:

Domyślny domyślny konstruktor dla klasy X jest definiowany jako usunięty, jeśli:

  • X to unia, która ma składową wariantu z nietrywialnym konstruktorem domyślnym i żaden element członkowski wariantu X nie ma domyślnego inicjatora elementu członkowskiego ,
  • X jest klasą nieunijną, która ma wariant składowy M z nietrywialnym konstruktorem domyślnym i żaden wariant elementu członkowskiego unii anonimowej zawierającej M ma domyślny inicjator elementu członkowskiego ,

Zwróć uwagę na podkreślone sformułowanie. W przykładzie IIUC, ponieważ element członkowski wariantu bma domyślny inicjator elementu członkowskiego, domyślny konstruktor domyślny nie powinien być definiowany jako usunięty. Dlaczego te kompilatory zgłaszają ten kod jako źle sformułowany?

Jeśli zmień definicję Cna

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

Następnie wszystkie kompilatory mogą skompilować ten kod. Zachowanie wskazuje, że reguła faktycznie oznacza:

X to unia, która ma element członkowski wariantu z nietrywialnym konstruktorem domyślnym i nie jest dostarczany domyślny inicjator elementu członkowskiego dla elementu członkowskiego wariantu

Czy to błąd kompilatora, czy niejasne sformułowanie tej reguły?

Odpowiedzi

6 ecatmur Dec 22 2020 at 16:24

Zostało to zmienione między C ++ 14 a C ++ 17, za pośrednictwem CWG 2084 , w którym dodano język umożliwiający NSDMI na (dowolnym) elemencie unii przywrócenie domyślnego konstruktora domyślnego.

Przykład towarzyszący CWG 2084 różni się nieco od twojego:

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

Tutaj NSDMI znajduje się na nietrywialnym składniku, podczas gdy sformułowanie przyjęte dla C ++ 17 pozwala NSDMI na dowolnym elemencie członkowskim na przywrócenie domyślnego konstruktora domyślnego. Dzieje się tak, ponieważ, jak napisano w tym DR,

NSDMI jest po prostu cukier syntaktyczny dla MEM-inicjatora

Oznacza to, że NSDMI on int b = 0;jest w zasadzie równoważne z napisaniem konstruktora z mem-initializer i pustym ciałem:

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

Nawiasem mówiąc , reguła zapewniająca, że co najwyżej jeden wariant członka związku ma NSDMI, jest nieco ukryta w podrozdziale class.union.anon :

4 - [...] Co najwyżej jeden wariant członka unii może mieć domyślny inicjator elementu członkowskiego.

Przypuszczam, że skoro gcc i Clang już zezwalają na powyższe (NSDMI na nietrywialnym członku związku), nie zdawali sobie sprawy, że muszą zmienić swoją implementację, aby uzyskać pełną obsługę C ++ 17.

Zostało to omówione na liście standardowej dyskusji w 2016 r. , Na przykładzie bardzo podobnym do twojego:

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

Wniosek był taki, że clang i gcc są wadliwe w odrzucaniu, chociaż w tym czasie istniała wprowadzająca w błąd uwaga, zmieniona w rezultacie.

W przypadku Clanga błąd jest https://bugs.llvm.org/show_bug.cgi?id=39686co zapętla nas z powrotem do SO w niejawnie zdefiniowanym konstruktorze usuniętym z powodu elementu członkowskiego wariantu, N3690 / N4140 vs N4659 / N4727 . Nie mogę znaleźć odpowiedniego błędu dla gcc.

Zauważ, że MSVC poprawnie akceptuje i inicjalizuje się cdo .b = 0, co jest poprawne dla dcl.init.aggr :

5 - [...] Jeśli agregat jest sumą, a lista inicjalizacyjna jest pusta, to

  • 5.4 - jeśli jakikolwiek element członkowski wariantu ma domyślny inicjator elementu członkowskiego, ten element członkowski jest inicjowany z jego domyślnego inicjatora elementu członkowskiego; […]
3 MichaëlRoy Dec 22 2020 at 17:30

Związki to trudna sprawa, ponieważ wszyscy członkowie mają tę samą przestrzeń pamięci. Zgadzam się, sformułowanie reguły nie jest wystarczająco jasne, ponieważ pomija to, co oczywiste: definiowanie wartości domyślnych dla więcej niż jednego członka związku jest niezdefiniowanym zachowaniem lub powinno prowadzić do błędu kompilatora.

Rozważ następujące:

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. 

To oczywiście nie powinno się kompilować.

Ten kod nie kompiluje się, ponieważ A nie ma jawnego konstruktora domyślnego.

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) 

Ten kod nie może się skompilować, ponieważ A :: x ma jawną wartość domyślną, koliduje to z jawną wartością domyślną dla U :: b (zamierzona gra słów).

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.

Ten kod nie skompiluje się również na gcc, mniej więcej z tego samego powodu, ale będzie działał na MSVC (MSVC jest zawsze nieco mniej rygorystyczny niż gcc, więc nie jest to zaskakujące):

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.

Jeśli chodzi o miejsce zgłaszania błędu, czy to w momencie deklaracji, czy w momencie tworzenia instancji, zależy to od kompilatora, gcc i msvc zgłaszają błąd w punkcie inicjalizacji, a clang zgłosi go, gdy spróbujesz utworzyć instancję unii.

Zauważ, że wysoce odradzane jest posiadanie członków unii, które nie są kompatybilne z bitami, a przynajmniej nie są z nimi powiązane. takie postępowanie łamie bezpieczeństwo typów i jest otwartym zaproszeniem do zgłaszania błędów w programie. Typ punning jest w porządku, ale w innych przypadkach należy rozważyć użycie std :: variant <>.