11 sept. 2007

Le futur standard C++ : les classes d'énumérations

Herb Sutter reviens du dernier meeting du WG21 - le workgroup normatif du langage C++ - et il semblerait que cette réunion ait été particulièrement productive. Un certain nombre de discussions importantes ont été sanctionnées par un vote d'approbation, il est donc temps d'en étudier certaines.

L'existant

Si à l'heure actuelle je souhaite définir un type qui possède les caractéristiques suivantes:

  • il doit se comporter grosso-modo comme une énumération
  • pas de conversion implicite vers le type int
  • la portée des identifiants définissant l'énumération est limitée à ce type (pour écrire, par exemple, type::enum_value, enum_value seul étant interdit)
  • je dois en connaître sa taille, au moins en relation avec un type intégral dont la taille est connue. L'idéal serait de connaître le type sous-jacent (si il est signé, non signé, quel est sa taille, etc).

Et bien je ne peux pas. Je peux trouver un moyen de contourner certain de ces problèmes - mais pas tous à la fois. Par exemple, je peux encapsuler une énumération dans une classe:

struct my_enum
{
  enum E { v1, v2, v3 };
};

Mais le type résultant (qui satisfait les points 1 et 3) reste implicitement convertible vers le type int - tout en ayant une taille indéterminée. Je peux complexifier à outrage ce type de classe, mais au final, il faudra que je sacrifie l'une de ces trois propositions - ou du moins certains de ses aspects (comme par exemple la possibilité d'utiliser les valeurs définies en tant que constantes dans une structure de contrôle switch).

les classes "enum" - proposition N2347 (pdf)

En remarquant les nombreux problèmes liés aux énumérations (portée des identifiants mal définie, conversion implicite, impossibilité de spécifier le type sous-jacent, etc), Middler/Sutter/Stoustrup ont proposé d'adopter dans le futur standard C++ la notion d'énumérations fortement typées déjà approuvée dans le standard C++/CLI. Dans la pratique, la proposition s'articule autour de deux modifications:

  • l'ajout de classes d'énumérations
  • l'extension de la signification du mot-clef enum (tout en gardant une compatibilité avec le code existant)

Une classe enum est définie ainsi (j'utilise ici une notation assez proche d'une BNF classique, donc vous ne devriez pas être perdu; E est la règle vide):

<enum-key> ::= enum class | enum struct
<enum-base-type-specifier'>' ::= :<integral-type-ident>'' | E
<enum-list> ::= <enum-definition> | <enum-definition>, <enum-list>
<enum-definition> ::= value-ident | value-ident = const-value 
<enum-decl> ::= <enum-key> type-ident <enum-base-type-specifier> { <enum-list> };

integral-type-ident est un type de donnée intégral (sauf wchar_t).

Les caractéristiques d'un tel type sont:

  1. il n'y a pas de conversion implicite vers le type int
  2. les valeurs définies sont définies dans la portée de la classe d'énumération
  3. si un enum-base-type-specifier (optionnel) est défini (ont dit alors qu'il est fixé), l'énumération aura la taille de ce type - mais plus important, chaque énumérateur aura pour type ce type de base[1]. Dans le cas contraire, le type de base d'une classe d'énumération est int (il est fixé aussi).

Considérons maintenant quelques exemples:

// first case: using the current standard enums
enum enum1 { red, blue, green, black, white };
enum enum2 { white, black }; // error (1)
enum1 e = red; // ok assert(sizeof(enum1) == sizeof(?)) // ? (2) int value = e; // ok
// first case: using class enums enum class color : short { red, blue, green, black, white }; enum class color2 : unsigned int { red, blue, green, black, white }; // valid
color my_color = red; // error (3) color my_color = color::red; // ok int value = my_color; // error (4) if (my_color) { ... } // error (5) assert(sizeof(color) == sizeof(short)); // ok (6)

Et disséquons tout ça:

  1. enum2 utilise les même identifiants qu'enum1. C'est interdit, car les identifiants d'une énumération n'ont pas de portée propre.
  2. la taille d'une énumération n'est pas spécifiée - elle est quasiment laissée à la libre interprétation du compilateur.
  3. il est impossible d'utiliser les identifiants définis dans une classe d'énumération sans faire référence à cette classe, car il ne sont définis que dans la portée de cette classe.
  4. il n'y a pas de conversion implicite vers le type int.
  5. il n'y a pas, non plus, de conversion implicite vers le type bool. En fait, il n'y a aucune conversion implicite vers un type intégral (integral promotion).
  6. toujours vrai, puisque le type de base de l'énumération color est short

Mais la proposition ne fait pas qu'étendre les possibilités du langage originel en rajoutant une nouvelle manière de déclarer un type. Elle modifie aussi la façon dont un type enum classique est traité, et redéfini certaines de ses propriétés:

<enum-key> ::= enum
<enum-base-type-specifier'>' ::= :<integral-type-ident>'' | E
<enum-list> ::= <enum-definition> | <enum-definition>, <enum-list>
<enum-definition> ::= value-ident | value-ident = const-value 
<enum-decl> ::= <enum-key> type-ident <enum-base-type-specifier> { <enum-list> };

Et oui, il est là aussi possible de définir (fixer) le type sous-jacent d'une énumération - bien que cela reste une option. Dans ce cas, les membres de l'énumération ont pour type ce type de base. Si il n'est pas défini, le type de base n'est pas fixé et les membre de l'énumération ont pour type le type de l'énumération[2].

Un type énuméré défini avec enum dans la future norme C++ sera strictement compatible compatible avec un type énuméré utilisant les règles actuelles: il continuera de suivre les règles de conversion implicite, la portée de sa définition reste la portée dans lequel il est défini, etc.

Conclusion

Vous remarquerez que j'ai utilisé la même pseudo-BNF pour décrire les deux fonctionnalités. C'est probablement le point fort de cette proposition : elle harmonise le fonctionnement des énumérations de manière globales, tout en fournissant des outils supplémentaires qui devraient faciliter l'écriture d'un code plus portable et plus sûr.

<enum-key> ::= enum | enum class | enum struct
<enum-base-type-specifier'>' ::= :<integral-type-ident>'' | E
<enum-list> ::= <enum-definition> | <enum-definition>, <enum-list>
<enum-definition> ::= value-ident | value-ident = const-value 
<enum-decl> ::= <enum-key> type-ident <enum-base-type-specifier> { <enum-list> };

Notes

[1] cela peut sembler être en contradiction avec le fait qu'il n'y ait pas de conversion implicite vers le type int. La non-conversion implicite est per jure, cette fonctionnalité est donc indépendant du type utilisé pour représenter les valeurs.

[2] je sais, ça peut paraitre un peu embrouillé toutes ces historie de types de base fixés ou non; il faut principalement se rappeler que le type de base d'une classe d'énumération est toujours fixé, tandis que le type de base d'une énumération classique n'est fixé que si il est explicitement défini.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet