¿Por qué se elimina el constructor predeterminado predeterminado para una unión o clase similar a una unión?
struct A{
A(){}
};
union C{
A a;
int b = 0;
};
int main(){
C c;
}
En el código anterior, GCC y Clang se quejan de que el constructor predeterminado para union C
está definido como eliminado.
Sin embargo, la regla relevante dice que:
Un constructor predeterminado predeterminado para la clase X se define como eliminado si:
- X es una unión que tiene un miembro variante con un constructor predeterminado no trivial y ningún miembro variante de X tiene un inicializador de miembro predeterminado ,
- X es una clase sin unión que tiene un miembro variante M con un constructor predeterminado no trivial y ningún miembro variante de la unión anónima que contiene M tiene un inicializador de miembro predeterminado ,
Note la redacción enfatizada. En el ejemplo, IIUC, dado que el miembro variante b
tiene un inicializador de miembro predeterminado, el constructor predeterminado predeterminado no debe definirse como eliminado. ¿Por qué estos compiladores informan que este código está mal formado?
Si cambia la definición de C
a
union C{
A a{};
int b;
};
Entonces todos los compiladores pueden compilar este código. El comportamiento sugiere que la regla en realidad significa:
X es una unión que tiene un miembro variante con un constructor predeterminado no trivial y no se proporciona ningún inicializador de miembro predeterminado para el miembro variante
¿Es esto un error del compilador o la redacción vaga de esa regla?
Respuestas
Esto se cambió entre C ++ 14 y C ++ 17, a través de CWG 2084 , que agregó el lenguaje que permite a un NSDMI en (cualquier) miembro de la unión restaurar el constructor predeterminado predeterminado.
Sin embargo, el ejemplo que acompaña a CWG 2084 es sutilmente diferente al suyo:
struct S {
S();
};
union U {
S s{};
} u;
Aquí, el NSDMI está en el miembro no trivial, mientras que la redacción adoptada para C ++ 17 permite que un NSDMI en cualquier miembro restaure el constructor predeterminado predeterminado. Esto se debe a que, como está escrito en ese DR,
Un NSDMI es básicamente azúcar sintáctico para un inicializador de mem
Es decir, el NSDMI activado int b = 0;
es básicamente equivalente a escribir un constructor con mem-initializer y cuerpo vacío:
C() : b{/*but use copy-initialization*/ 0} {}
Como acotación al margen, la regla que asegura que como máximo un miembro variante del sindicato tiene un NSDMI está algo oculta en una subcláusula de class.union.anon :
4 - [...] Como máximo, una variante de miembro de un sindicato puede tener un inicializador de miembro predeterminado.
Mi suposición sería que dado que gcc y Clang ya permiten lo anterior (el NSDMI en el miembro de la unión no trivial) no se dieron cuenta de que necesitan cambiar su implementación para soporte completo de C ++ 17.
Esto se discutió en la lista std-discusion en 2016 , con un ejemplo muy similar al suyo:
struct S {
S();
};
union U {
S s;
int i = 1;
} u;
La conclusión fue que clang y gcc son defectuosos al rechazar, aunque en ese momento había una nota engañosa, modificada como resultado.
Para Clang, el error es https://bugs.llvm.org/show_bug.cgi?id=39686que nos devuelve a SO en Constructor definido implícitamente eliminado debido a un miembro variante, N3690 / N4140 vs N4659 / N4727 . No puedo encontrar un error correspondiente para gcc.
Tenga en cuenta que MSVC acepta correctamente , e inicializa c
a .b = 0
, lo que es correcto por dcl.init.aggr :
5 - [...] Si el agregado es una unión y la lista de inicializadores está vacía, entonces
- 5.4 - si algún miembro variante tiene un inicializador de miembro predeterminado, ese miembro se inicializa desde su inicializador de miembro predeterminado; [...]
Las uniones son algo complicado, ya que todos los miembros comparten el mismo espacio de memoria. Estoy de acuerdo, la redacción de la regla no es lo suficientemente clara, ya que omite lo obvio: la definición de valores predeterminados para más de un miembro de una unión es un comportamiento indefinido o debería conducir a un error del compilador.
Considera lo siguiente:
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, esto no debería compilarse.
Este código se compila porque A no tiene un constructor predeterminado 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 no se puede compilar, porque A :: x tiene un valor predeterminado explícito, esto choca con el valor predeterminado explícito para U :: b (juego de palabras).
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 tampoco se compilará en gcc, aproximadamente por la misma razón, pero funcionará en MSVC (MSVC siempre es un poco menos estricto que gcc, por lo que no es sorprendente):
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.
En cuanto a dónde se informa el error, ya sea en el punto de declaración o instanciación, esto depende del compilador, gcc y msvc informan del error en el punto de inicialización, y clang lo informará cuando intente instanciar la unión.
Tenga en cuenta que es muy desaconsejable tener miembros de un sindicato que no sean compatibles con bits o, al menos, que se puedan relacionar un poco. al hacerlo, se rompe la seguridad de los tipos y es una invitación abierta a que haya errores en su programa. Escribir juegos de palabras está bien, pero para otros casos de uso, uno debería considerar usar std :: variant <>.