Por que o construtor padrão padronizado é excluído para uma classe union ou semelhante a uma união?

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

No código acima, GCC e Clang reclamam que o construtor padrão para união Cé definido como excluído.

No entanto, a regra relevante diz que:

Um construtor padrão padronizado para a classe X é definido como excluído se:

  • X é uma união que tem um membro variante com um construtor padrão não trivial e nenhum membro variante de X tem um inicializador de membro padrão ,
  • X é uma classe não-união que tem um membro variante M com um construtor padrão não trivial e nenhum membro variante da união anônima contendo M tem um inicializador de membro padrão ,

Observe o texto enfatizado. No exemplo, IIUC, uma vez que o membro variante btem um inicializador de membro padrão, o construtor padrão padrão não deve ser definido como excluído. Por que esses compiladores relatam esse código como malformado?

Se alterar a definição de Cpara

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

Então, todos os compiladores podem compilar esse código. O comportamento sugere que a regra realmente significa:

X é uma união que tem um membro variante com um construtor padrão não trivial e nenhum inicializador de membro padrão é fornecido para o membro variante

Isso é um bug do compilador ou o texto vago dessa regra?

Respostas

6 ecatmur Dec 22 2020 at 16:24

Isso foi alterado entre C ++ 14 e C ++ 17, por meio do CWG 2084 , que adicionou a linguagem que permite que um NSDMI em (qualquer) membro do sindicato restaure o construtor padrão padrão.

O exemplo que acompanha o CWG 2084, embora seja sutilmente diferente do seu:

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

Aqui, o NSDMI está no membro não trivial, enquanto o texto adotado para C ++ 17 permite que um NSDMI em qualquer membro restaure o construtor padrão padrão. Isso ocorre porque, conforme escrito nesse DR,

Um NSDMI é basicamente um açúcar sintático para um inicializador de mem

Ou seja, o NSDMI ativado int b = 0;é basicamente equivalente a escrever um construtor com inicializador de mem e corpo vazio:

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

Como um aparte, a regra que garante que no máximo um membro variante do sindicato tenha um NSDMI está um tanto oculta em uma subseção de class.union.anon :

4 - [...] no máximo um membro variante de um sindicato pode ter um inicializador de membro padrão.

Minha suposição seria que, uma vez que o gcc e o Clang já permitem o acima (o NSDMI no membro do sindicato não trivial), eles não perceberam que precisam alterar sua implementação para obter suporte total ao C ++ 17.

Isso foi discutido na lista std-Discussion em 2016 , com um exemplo muito semelhante ao seu:

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

A conclusão foi que clang e gcc são defeituosos na rejeição, embora houvesse na época uma nota enganosa, alterada como resultado.

Para o Clang, o bug é https://bugs.llvm.org/show_bug.cgi?id=39686que nos faz um loop de volta ao SO em Construtor implicitamente definido excluído devido ao membro variante, N3690 / N4140 vs N4659 / N4727 . Não consigo encontrar um bug correspondente para o gcc.

Note-se que MSVC aceita corretamente , e inicializa ca .b = 0, que é correta por dcl.init.aggr :

5 - [...] Se o agregado for uma união e a lista de inicializadores estiver vazia, então

  • 5.4 - se qualquer membro variante tiver um inicializador de membro padrão, esse membro é inicializado a partir de seu inicializador de membro padrão; [...]
3 MichaëlRoy Dec 22 2020 at 17:30

Os sindicatos são complicados, pois todos os membros compartilham o mesmo espaço de memória. Concordo, o texto da regra não é suficientemente claro, pois deixa de fora o óbvio: definir valores padrão para mais de um membro de um sindicato é um comportamento indefinido ou deve levar a um erro do compilador.

Considere o seguinte:

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. 

Obviamente, isso não deve ser compilado.

Este código é compilado, porque A não tem um construtor padrão explícito.

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) 

Este código não pode ser compilado, porque A :: x tem um valor padrão explícito, isso colide com o valor padrão eplícito de U :: b (trocadilho intencional).

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.

Este código também não será compilado no gcc, pelo mesmo motivo, mas funcionará no MSVC (o MSVC é sempre um pouco menos rígido do que o gcc, por isso não é surpreendente):

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.

Quanto ao local onde o erro é relatado, seja no ponto de declaração ou instanciação, isso depende do compilador, gcc e msvc relatam o erro no ponto de inicialização e o clang relatará quando você tentar instanciar a união.

Observe que é altamente desaconselhável ter membros de um sindicato que não sejam compatíveis com bits ou, pelo menos, pouco relacionáveis. fazer isso interrompe o tipo de segurança e é um convite aberto para bugs em seu programa. O trocadilho de tipo está OK, mas para outros casos de uso, deve-se considerar o uso de std :: variant <>.