5
votes

Énumération non scopée, énumérateur et ambiguïté de type sous-jacent en C ++

Je passais par la norme C ++ n4713.pdf. Considérez le code ci-dessous:

#include <iostream>
#include <type_traits>

enum UEn
{
    EN_0,
    EN_1,
    EN_L = 0x7FFFFFFFFFFFFFFF            // EN_L has type "long int"
};                                       // UEn has underlying type "unsigned long int"

int main()
{
    long lng = 0x7FFFFFFFFFFFFFFF;

    std::cout << std::boolalpha;
    std::cout << "typeof(unsigned long == UEn):" << std::is_same<unsigned long, std::underlying_type<UEn>::type>::value << std::endl;  // Outputs "true"
    std::cout << "sizeof(EN_L):" << sizeof(EN_L) << std::endl;
    std::cout << "sizeof(unsigned):" << sizeof(unsigned) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long long) << std::endl;

    lng = EN_L + 1;                      // Invokes UB as EN_L is 0x7FFFFFFFFFFFFFFF and has type "long int"

    return 0;
}

Les sorties de code ci-dessus (testées sur g ++ - 8.1, Clang):

typeof (unsigned long == UEn): true sizeof (EN_L): 8 sizeof (unsigned): 4 sizeof (unsigned long): 8 sizeof (unsigned long) ): 8

Selon la section 10.2p5 (10.2 Déclarations d'énumération):

Après l'accolade fermante d'un spécificateur d'énumération, chaque énumérateur a le type de son énumération ... Si le type sous-jacent n'est pas fixe, le le type de chaque énumérateur avant l'accolade fermante est déterminé comme suit:

  • Si un initialiseur est spécifié pour un énumérateur, le expression-constante doit être une expression constante intégrale (8.6). Si l'expression a un type d'énumération non délimité, l'énumérateur a le type sous-jacent de ce type d'énumération, sinon il a le même tapez comme expression.

  • Si aucun initialiseur n'est spécifié pour le premier énumérateur, son type est un type intégral signé non spécifié.

  • Sinon, le type de l'énumérateur est le même que celui du énumérateur précédent sauf si la valeur incrémentée n'est pas représentable dans ce type, auquel cas le type est un type intégral non spécifié suffisant pour contenir la valeur incrémentée. Si un tel type n'existe pas, le programme est mal formé.

De plus, la section 10.2p7 déclare:

Pour une énumération dont le type sous-jacent n'est pas fixe, le sous-jacent type est un type intégral qui peut représenter toutes les valeurs de l'énumérateur défini dans l'énumération. Si aucun type intégral ne peut représenter tous les énumérateur, l'énumération est mal formée. Il est définition de l'implémentation quel type d'intégrale est utilisé comme sous-jacent type sauf que le type sous-jacent ne doit pas être plus grand que int sauf si la valeur d'un énumérateur ne peut pas tenir dans un int ou unsigned int.


J'ai donc les questions suivantes:

  1. Pourquoi le type sous-jacent d'énumération UEn est-il un unsigned long lorsque 0x7FFFFFFFFFFFFFFF est une constante entière de type long int code> et donc le type de EN_L est également long int . Est-ce un bogue du compilateur ou un comportement bien défini?
  2. Lorsque la norme dit que chaque énumérateur a le type de son énumération , cela ne devrait-il pas impliquer que les types intégraux d'énumérateur et d'énumération doivent également correspondre? Quelle pourrait être la raison pour laquelle ces deux éléments sont différents l'un de l'autre?


3 commentaires

0x7FFFFFFFFFFFFFFF est une constante intégrale de signature indéterminée. Votre compilateur a choisi de le stocker sous la forme d'un long non signé .


@JonHarper: La norme dit: "Le type d'un littéral entier est le premier de la liste correspondante dans le tableau 7 dans laquelle sa valeur peut être représentée: Littéral binaire, octal ou hexadécimal - int, unsigned int, long int , unsigned long int, long long int, unsigned long long int ". Le compilateur ne peut pas agir à sa guise, n'est-ce pas?


Un compilateur conforme peut agir à sa guise dans certains cas (UB, comportement non spécifié), mais sinon vous avez raison. Cependant, les compilateurs sont rarement, voire jamais, vraiment conformes.


3 Réponses :


2
votes

Le type sous-jacent est défini par l'implémentation. Il doit seulement être capable de représenter tous les énumérateurs, et il ne peut pas être plus grand que int sauf si nécessaire. Il n'y a aucune exigence sur la signature (mis à part le fait que le type de base doit pouvoir représenter chaque énumérateur), selon dcl.enum.7 , comme vous l'avez déjà trouvé. Cela limite la rétro-propagation des types d'énumérateurs plus que vous ne semblez le supposer. Notamment, il ne dit nulle part que le type de base de l'énumération doit être le type de l'un des initialiseurs des énumérateurs.

Clang préfère les entiers non signés comme bases d'énumération aux entiers signés; c'est tout ce qu'on peut en dire. Il est important de noter que le type de l'énumération ne doit pas nécessairement correspondre au type d'un énumérateur spécifique: il doit seulement être capable de représenter chaque énumérateur. C'est assez normal et bien compris dans d'autres contextes. Par exemple, si vous aviez EN_1 = 1 , cela ne vous surprendrait pas que le type de base de l'énumération ne soit pas int ou unsigned int , même si 1 est un int.

Vous avez également raison de dire que le type de 0x7fffffffffffffff est long . Clang est d'accord avec vous, mais convertit implicitement la constante en unsigned long :

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff
};

int main() {
    tell_me(Baz);
    // call    Foo tell_me<Foo>(Foo const&&)
}

Ceci est autorisé, car comme nous l'avons déjà dit, le type de base de l'énumération n'a pas besoin d'être le type textuel de n'importe quel énumérateur.

Quand la norme dit que chaque énumérateur a le type de l'énumération, cela signifie que le type de EN_1 est enum UEn après l'accolade de fermeture de l'énumération. Notez les mentions «après l'accolade fermante» et «avant l'accolade fermante».

Avant l'accolade fermante, si l'énumération n'a pas de type fixe, alors le type de chaque énumérateur est celui de son initialisation type d'expression, mais ce n'est que temporaire. C'est ce qui vous permet, par exemple, d'écrire EN_2 = EN_1 + 1 sans lancer EN_1 , même dans le cadre d'une classe enum . Ce n'est plus vrai après l'accolade fermante. Vous pouvez tromper le compilateur en vous montrant en inspectant les messages d'erreur ou en regardant le désassemblage:

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff,
    Frob = tell_me(Baz)
    // non-constexpr function 'tell_me<long>' cannot be used in a constant expression
};

Notez que dans ce cas, T a été déduit comme étant long , mais après l'accolade fermante, il est supposé être Foo:

TranslationUnitDecl
`-EnumDecl <line:1:1, line:5:1> line:1:6 Foo
  |-EnumConstantDecl <line:2:5> col:5 Frob 'Foo'
  |-EnumConstantDecl <line:3:5> col:5 Bar 'Foo'
  `-EnumConstantDecl <line:4:5, col:11> col:5 Baz 'Foo'
    `-ImplicitCastExpr <col:11> 'unsigned long' <IntegralCast>
      `-IntegerLiteral <col:11> 'long' 576460752303423487

Si vous voulez que votre type d'énumération être signé avec Clang, vous devez le spécifier en utilisant la syntaxe : base_type , ou vous devez avoir un énumérateur négatif.


2 commentaires

Les valeurs d'énumération négatives sont de toute façon utiles pour indiquer les codes d'erreur. Chaque fois que je crée un nouvel Enum, je spécifie toujours error = -1 comme première entrée. Plus tard, vous pouvez comparer if code> SomeEnum :: error . Cependant, c'est une bonne idée d'utiliser la syntaxe : base_type pour éviter les erreurs de logique arithmétique entre les types signés et non signés, comme vous le suggérez. Il serait déroutant s'il y avait des énumérations signées et des énumérations non signées, dans le cas d'énumérations sans valeurs négatives définies.


@zneak Je suis d'accord, mais le point que j'essayais de faire est que même si tous les énumérateurs pouvaient tenir dans long, le compilateur imposerait explicitement un type non signé à l'énumération. Ne serait-il pas judicieux de conserver le même type que le type des énumérateurs tant qu'il ne peut pas leur convenir. Pour couronner le tout, GCC et Clang ont le même comportement. Eh bien, je suppose que si la norme dit que la mise en œuvre est définie, le point est sans objet! Btw, +1 pour l'astuce avec les modèles!



2
votes

Je pense que la réponse à cet avertissement (certes peu intuitif) se trouve dans 7.6 Promotions intégrales [conv.prom]:

Une prvalue d'un type d'énumération non scopé dont le type sous-jacent n'est pas fixed (10.2) peut être converti en une valeur pr de la première des les types suivants qui peuvent représenter toutes les valeurs de l'énumération (c'est-à-dire les valeurs comprises entre b min et b max comme décrit en 10.2): int , unsigned int , long int , unsigned long int , long long int , ou unsigned long long int .

Par exemple, si votre type sous-jacent n'est pas fixe, et que vous utilisez un membre d'énumération dans une expression, il ne se convertit pas nécessairement en type sous-jacent de l'énumération. Il se convertit plutôt en le premier type de cette liste dans lequel tous les membres rentrent.

Ne me demandez pas pourquoi, la règle me semble folle.

Cette section continue en disant :

Une prvalue d'un type d'énumération non scopé dont le type sous-jacent est fixed (10.2) peut être converti en une valeur pr de son type sous-jacent.

C'est-à-dire si vous corrigez le type sous-jacent avec unsigned long :

EN_2 = 0x8000000000000000

alors l'avertissement disparaît.

Une autre façon de se débarrasser de l'avertissement (et laisser le type sous-jacent non fixé) est d'ajouter un membre qui nécessite un stockage unsigned long :

enum UEn : unsigned long
...

Là encore, l'avertissement s'en va.

Bonne question. J'ai beaucoup appris en y répondant.


1 commentaires

Cela signifie donc que le type sous-jacent d'une énumération est fixé uniquement dans les 2 cas suivants - 1. Énumération à portée 2. Sans portée avec le qualificateur "enum base", (Sec 10.2p5): "Chacun l'énumération a également un type sous-jacent. Le type sous-jacent peut être spécifié explicitement à l'aide d'une enum-base. Pour un type d'énumération à portée, le type sous-jacent est int s'il n'est pas spécifié explicitement. Dans ces deux cas, le type sous-jacent est dit être corrigé ". Ainsi, bien que le type sous-jacent d'enum soit "unsigned", les "7.6 promotions intégrales" font que l'incrément est UB! Garçon, je ne sais pas comment réagir. C'est le bordel!



0
votes

Le libellé de la section 10.2p5 dit explicitement "... avant l'accolade fermante ..." suggère l'interprétation suivante. Le type d'un énumérateur dans la définition du type enum (avant l'accolade fermante) est choisi pour être un type entier suffisamment grand pour représenter sa valeur. Cette valeur peut ensuite être réutilisée dans la définition de la définition des énumérateurs suivants dans la même énumération. Lorsque l'accolade fermante de type enum est rencontrée, le compilateur choisit un type intégral suffisamment grand pour représenter toutes les valeurs de l'énumérateur. Après la définition du type enum, toutes les valeurs de l'énumérateur ont le même type (qui est le type enum) et partagent le type sous-jacent de l'énumération. Par exemple:

i
2E1
2E1
2E1

Ran avec clan5 et gcc8 et il renvoie:

#include <iostream>
#include <typeinfo>
#include <type_traits>

enum E1
{
  e1 = 0, // type of the initializer (int), value = 0
  e2 = e1 + 1U, // type of the initializer (unsigned = int + unsigned), value = 1U
  e3 = e1 - 1, // type of the initializer (int = int - int), value = -1
}; // range of values [-1, 1], underlying type is int

int main()
{
   std::cout << typeid(std::underlying_type<E1>::type).name() << '\n';
   std::cout << typeid(e1).name() << '\n';
   std::cout << typeid(e2).name() << '\n';
   std::cout << typeid(e3).name() << '\n';
}


0 commentaires