Pourquoi le constructeur par défaut par défaut est-il supprimé pour une classe de type union ou union?
struct A{
A(){}
};
union C{
A a;
int b = 0;
};
int main(){
C c;
}
Dans le code ci-dessus, GCC et Clang se plaignent tous deux que le constructeur par défaut pour union C
est défini comme supprimé.
Cependant, la règle pertinente dit que:
Un constructeur par défaut pour la classe X est défini comme supprimé si:
- X est une union qui a un membre variant avec un constructeur par défaut non trivial et aucun membre variant de X n'a un initialiseur de membre par défaut ,
- X est une classe non-union qui a un membre variant M avec un constructeur par défaut non trivial et aucun membre variant de l'union anonyme contenant M n'a un initialiseur de membre par défaut ,
Remarquez le libellé souligné. Dans l'exemple IIUC, puisque le membre variant b
a un initialiseur de membre par défaut, le constructeur par défaut par défaut ne doit pas être défini comme supprimé. Pourquoi ces compilateurs signalent-ils ce code comme mal formé?
Si changer la définition de C
en
union C{
A a{};
int b;
};
Ensuite, tous les compilateurs peuvent compiler ce code. Le comportement indique que la règle signifie en fait:
X est une union qui a un membre variant avec un constructeur par défaut non trivial et aucun initialiseur de membre par défaut n'est fourni pour le membre variant
S'agit-il d'un bogue du compilateur ou du libellé vague de cette règle?
Réponses
Cela a été changé entre C ++ 14 et C ++ 17, via CWG 2084 , qui a ajouté le langage permettant à un NSDMI sur (n'importe quel) membre d'union de restaurer le constructeur par défaut par défaut.
L'exemple accompagnant le CWG 2084 est cependant légèrement différent du vôtre:
struct S {
S();
};
union U {
S s{};
} u;
Ici, le NSDMI est sur le membre non trivial, alors que le libellé adopté pour C ++ 17 permet à un NSDMI sur n'importe quel membre de restaurer le constructeur par défaut par défaut. C'est parce que, comme écrit dans ce DR,
Un NSDMI est fondamentalement un sucre syntaxique pour un initialiseur de mémoire
Autrement dit, le NSDMI sur int b = 0;
équivaut fondamentalement à écrire un constructeur avec mem-initializer et un corps vide:
C() : b{/*but use copy-initialization*/ 0} {}
En passant, la règle garantissant qu'au plus une variante membre de l'union a un NSDMI est quelque peu cachée dans un sous-paragraphe de class.union.anon :
4 - [...] Au plus un membre variant d'une union peut avoir un initialiseur de membre par défaut.
Ma supposition serait que puisque gcc et Clang autorisent déjà ce qui précède (le NSDMI sur le membre non trivial du syndicat), ils ne se sont pas rendu compte qu'ils devaient changer leur implémentation pour une prise en charge complète de C ++ 17.
Cela a été discuté sur la liste std-discussion en 2016 , avec un exemple très similaire au vôtre:
struct S {
S();
};
union U {
S s;
int i = 1;
} u;
La conclusion était que clang et gcc sont défectueux dans le rejet, bien qu'il y ait eu à l'époque une note trompeuse, modifiée en conséquence.
Pour Clang, le bug est https://bugs.llvm.org/show_bug.cgi?id=39686qui nous renvoie à SO au constructeur implicitement défini supprimé en raison du membre variant, N3690 / N4140 vs N4659 / N4727 . Je ne trouve pas de bogue correspondant pour gcc.
Notez que MSVC accepte correctement , et initialise c
à .b = 0
, ce qui est correct par dcl.init.aggr :
5 - [...] Si l'agrégat est une union et que la liste d'initialisation est vide, alors
- 5.4 - si un membre variant a un initialiseur de membre par défaut, ce membre est initialisé à partir de son initialiseur de membre par défaut; [...]
Les syndicats sont une chose délicate, car tous les membres partagent le même espace mémoire. Je suis d'accord, le libellé de la règle n'est pas assez clair, car il laisse de côté l'évidence: définir des valeurs par défaut pour plus d'un membre d'un syndicat est un comportement indéfini, ou devrait conduire à une erreur du compilateur.
Considérer ce qui suit:
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.
Cela ne devrait évidemment pas compiler.
Ce code compile, car A n'a pas de constructeur par défaut explicite.
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)
Ce code ne peut pas être compilé, car A :: x a une valeur par défaut explicite, cela entre en conflit avec la valeur par défaut explicite pour U :: b (jeu de mots prévu).
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.
Ce code ne se compilera pas non plus sur gcc, pour à peu près la même raison, mais fonctionnera sur MSVC (MSVC est toujours un peu moins strict que gcc, donc ce n'est pas surprenant):
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.
Quant à l'endroit où l'erreur est signalée, au point de déclaration ou d'instanciation, cela dépend du compilateur, gcc et msvc signalent l'erreur au point d'initialisation, et clang le signalera lorsque vous essayez d'instancier l'union.
Notez qu'il est fortement déconseillé d'avoir des membres d'une union qui ne sont pas compatibles avec les bits, ou à tout le moins peu fiables. cela brise la sécurité des types et constitue une invitation ouverte aux bogues dans votre programme. La punition de type est OK, mais pour d'autres cas d'utilisation, il faut envisager d'utiliser std :: variant <>.