C++20 – Les designated initializers

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 :

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 :

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 :

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.

De plus, il est impossible de mixer les listes d’initialisation classiques et les designated intializers :

En raison du conflit syntaxique avec les lambdas, il n’est pas possible d’utiliser les array-designators du :

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) :

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 :

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 :

Une autre solution classique est le named parameter idiom, qui s’appuie sur le chaînage des méthodes.

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 :

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 :

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 :).

Références et ressources

%d bloggers like this: