Dlaczego domyślny konstruktor domyślny jest usuwany z klasy unii lub unii?
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 C
jest 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 b
ma 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ę C
na
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
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ę c
do .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; […]
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 <>.