Por que o construtor padrão padronizado é excluído para uma classe union ou semelhante a uma união?
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 b
tem 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 C
para
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
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 c
a .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; [...]
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 <>.