Salut à tous.
Cet article traite d’une fonctionnalité à venir pour C++20 : les designated initialisers.
Déjà présente en C depuis C99, la version adoptée pour la prochaine norme C++ présente quelques différences.
Les designated initialisers
La p0329 propose d’ajouter les designated initializers au langage et a été acceptée pour C++20. Ils sont déjà disponibles avec GCC 8.2 (sorti fin juillet) et partiellement sur Clang.
De façon similaire à C99, ils permettent d’initialiser seulement certains éléments d’une structure :
1 2 3 4 5 6 7 8 |
struct Point { int i = 1; int j = 1; }; Point p{ .j = 3 }; // p = {1, 3}. Point p{ .i = 21, .j{} }; // p = {21, 0}. |
Lors d’une initialisation avec accolades (braced-init-list), il est désormais possible d’utiliser une liste d’initialisation contenant des designated initializers plutôt qu’une liste d’initialisation classique. Un designator est un élément de cette liste.
Outre le fait qu’il est parfois intéressant de ne spécifier que quelques membres, cette nouveauté simplifie la syntaxe pour les unions, pour lesquelles il n’était pas possible d’initialiser autre chose que le premier champ :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
union U { int i; double d; }; // U u{21, 2.0}; // GCC 8.2 : error : too many intializers for 'U'. U u{2.0}; // Initialise i avec le litéral flottant 2.0 (conversion implicite). // Pre C++20 : U u; u.d = 2.0; // C++20 avec designated initializers. U u{ .d = 2.0 }; |
L’ajout des designated initializers est également un gain de compatibilité avec le C, même si quelques différences subsistent entre les deux spécifications.
Comparaison avec C99
En C, l’ordre d’initialisation des arguments est de gauche à droite, comme en C++. En revanche, l’ordre d’évaluation est non spécifié. Puisque C++ spécifie que l’évaluation d’une braced-init-list est de gauche à droite, c’est aussi le cas lorsqu’il s’agit de designated initializers.
De plus, puisque les destructeurs seront appelés dans l’ordre inverse de construction. Il est impératif que les membres soient désignés au sein de la designated-initializer-list dans le même ordre que celui de leur déclaration :
1 2 3 4 5 6 7 8 |
struct Foo { int a; double b; std::string s; }; Foo f{ .d = 2., .i = 2 }; // GCC 8.2 : error: designator order for field 'Foo::i' does not match declaration order in 'Foo'. |
En C, il est possible de spécifier plusieurs fois le même designator, ce qui est interdit en C++. En effet, c’est assez peu utile (honnêtement j’ai du mal à voir un cas d’usage) et ça peut poser des problèmes lorsqu’un constructeur a des effets de bord.
1 |
Foo f{ .i = 1, .j = 2}; // GCC 8.2 : error: '.i' designator used multiple times in the same initializer list |
De plus, il est impossible de mixer les listes d’initialisation classiques et les designated intializers :
1 |
Foo f{0, .d = 2}; // GCC 8.2 : error: either all initializer clauses should be designated or none of them should be |
En raison du conflit syntaxique avec les lambdas, il n’est pas possible d’utiliser les array-designators du C :
1 |
int arr[5] = {[2] = 12}; // GCC 8.2 : warning: ISO C++ does not allow C99 designated initializers [-WPendantic] |
Enfin, il n’est pas possible d’utiliser les nested designator (designateurs imbriqués). Ils sont assez peu utilisés (d’après le document) et il est possible de s’en sortir avec les imbrications de listes d’initialisation (puisque le C++ autorise l’initialisation des designators avec accolades) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct Bar { int i; double d; }; struct Foo { int i; Bar b; }; Foo f1{ .b.i = 12 }; // GCC 8.2 : error: expected-primary-expression before '.' token... (bon le message d'erreur est pas terrible ici). Foo f2{ .b{ .i = 12 } }; // Ok. |
Le support de C++ est donc un peu restreint par rapport à la version de C99.
Avant de nous quitter, je vous propose une petite digression sur les paramètres nommés.
Les paramètres nommés (named parameters)
Qu’est-ce que les paramètres nommés ? Si vous ne les avez jamais rencontrés dans aucun langage, il s’agit de pouvoir spécifier seulement quelques arguments d’une fonction en les nommant, de la même manière que pour les designated initializers.
Prenons un exemple tout bête :
1 |
void launch_game_window(display_mode display = display_mode::FULLSCREEN, dimensions dims = {1920, 1080}, unsigned framerate = 60, unsigned screen = 0, bool v_sync = false, antialiasing_mode aa_mode = antialiasing_mode::FXAA); |
Ici, l’ordre des paramètres est très important. Il est possible de spécifier uniquement le mode d’affichage, mais pour spécifier uniquement la synchronisation verticale on se retrouve à devoir recopier les valeurs par défaut des 4 premiers paramètres.
Comment laisser le choix à l’utilisateur de ne spécifier que 1, 2 ou 3 arguments ? Doit-on écrire 6! = 720 surcharges ?
Bon en réalité, si on veut écrire toutes les surcharges pour laisser toutes les possibilités à l’utilisateur, on n’aura pas autant de surcharges à écrire puisque justement les arguments par défaut réduisent ce nombre. Mais il nous reste encore un bon nombre de fonctions à écrire.
Dans un cas comme celui-ci plusieurs options s’offrent à nous en C++.
Une première idée serait de passer par une structure contenant les paramètres qui serait passée à la fonction. C’est surement la solution la plus logique :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct graphic_params { display_mode display = display_mode::FULLSCREEN; dimensions dims = {1920, 1080}; unsigned framerate = 60; unsigned screen = 0; bool v_sync = false; antialisaing_mode aa_mode = antialiasing_mode::FXAA; }; void launch_game_window(const graphic_params& params); /* ... */ // Initialisation des paramètres. graphic_params params; params.display = display_mode::FULLSCREEN_WINDOWED; params.v_sync = true; params.aa_mode = antialiasing_mode::MSAA; launch_game_window(params); |
Une autre solution classique est le named parameter idiom, qui s’appuie sur le chaînage des méthodes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class graphic_params { public: graphic_params() = default; graphic_params& display(display_mode dm) { display_ = dm; return *this; } graphic_params& dims(dimensions dims) { dims_ = dims; return *this; } graphic_params& framerate(unsigned framerate) { framerate_ = framerate; return *this; } graphic_params& screen(unsigned screen) { screen_ = screen; return *this; } graphic_params& enable_v_sync() { v_sync_ = true; return *this; } graphic_params& aa_mode(antialiasing_mode aa_mode) { aa_mode_ = aa_mode; return *this; } private: display_mode display_ = display_mode::FULLSCREEN; dimensions dims_ = {1920, 1080}; unsigned framerate_ = 60; unsigned screen_ = 0; bool v_sync_ = false; antialiasing_mode aa_mode_ = antialiasing_mode::FXAA; }; void launch_game_window(const graphic_params& params); /* ... */ launch_game_window(graphic_params{} .display(display_mode::WINDOWED) .enable_v_sync() .aa_mode(antialiasing_mode::MSAA)); |
C’est un peu lourd à écrire au niveau du design de l’interface, mais à l’usage ca permet d’achever ce qu’on voulait. L’initialisation se fait sur une seule ligne contrairement à la version précédente. Notez qu’avec l’inlining des fonction, le code généré devrait être le même pour les deux versions.
Ce qu’on aimerait, pour se simplifier la vie, c’est avoir les named parameters intégrés au langage. Par exemple, en Python, il est possible d’utiliser une fonction de la sorte :
1 2 3 4 |
def load(display_mode = "FULLSCREEN", dims_w = 1920, dims_h = 1080, framerate = 60, screen = 0, v_sync = False, aa_mode = "FXAA"): # ... load(display_mode = "WINDOWED", v_sync = True, aa_mode = "MSAA"); |
Sachez qu’il y a une proposition pour cela en C++, la n4172. Je n’arrive pas à savoir si elle est toujours en attente ou si elle a été refusée, mais les noms de paramètres n’étant pas standardisés en C++ et pouvant être modifié dans l’implémentation ou à chaque redéclaration, la mise en place d’une telle fonctionnalité demanderait de changer pas mal de choses.
Cependant, les designated initializers permettent de simuler les named parameters en grande partie. Si on reprends la première version avec une structure de paramètres, il nous est possible de faire :
1 2 3 |
launch_game_window({.display = display_mode::FULLSCREEN_WINDOWED, .v_sync = true, .aa_mode = antialiasing_mode::MSAA}); |
Ca ressemble pas mal n’est-ce pas ?
Alors bien sûr, si vous utilisez une vieille bibliothèque avec des fonctions contenant 30 arguments avec tous une valeur par défaut ça ne fonctionnera paset il faut tout de même que l’interface soit adaptée pour être utilisée ainsi (par exemple avec une structure de paramètres).
Voilà donc pour les designated initializers qui sont mine de rien une fonctionnalité qui s’avère bien pratique dans certains cas. Je vous dis à bientôt pour un nouvel article :).
You must log in to post a comment.