Ce qui suit est une ébauche d'un article à venir.
Depuis C++ 17, il est possible de « déstructurer » un agrégat en plusieurs variables à l'aide d'une affectation déstructurante, ou Structured Binding de son nom technique.
Un exemple simple et un peu naïf serait le programme proposé à droite, où un Point nommé pt est utilisé pour quelques fins non-déterminées (le ... placé en commentaire). L'expression suivante :
... crée deux variables x et y qui seront des copies des attributs pt.x et pt.y respectivement (le lien se fait par la position des attributs, pas par les noms). Cet exemple est trop simple pour démontrer la force du mécanisme, évidemment. Notez tout d'abord qu'il aurait été possible de saisir des références aux attributs pt.x et pt.y en écrivant plutôt ceci :
|
|
L'utilité la plus directe des Structured Bindings est la déconstruction des valeurs retournées par les fonctions. Un ancêtre de ce mécanisme est la fonction std::tie() des std::tuple, par laquelle il est possible de décomposer un std::tuple en variables individuelles, ignorant (par std::ignore) les éléments qui ne sont pas utiles dans le contexte. Cela fonctionne d'ailleurs tout autant avec les std::pair.
À titre d'exemple (un peu tiré par les cheveux), prenons la classe Personne à droite, qui n'exposerait pas de service de consultation des états sur une base individuelle (pas d'accesseur spécifiquement prévu pour le nom ou pour l'âge), mais qui exposerait un service state() retournant un std::tuple contenant à lui seul l'ensemble des attributs. Il est possible d'extraire le nom de ce std::tuple de plusieurs manières :
Notez que la technique reposant sur std::tie() a un défaut, soit imposer la déclaration au préalable des variables dans lesquelles l'affectation sera effectuée, ce qui coûte l'exécution d'un constructeur par défaut a priori inutile.
|
|
Les Structured Bindings viennent donc couvrir (au moins en partie) cette niche : en combinant la décomposition de l'agrégat et la déclaration des variables, le passage par un objet construit par défaut mais sans réelle raison perd sa raison d'être.
Tel que vu plus haut, il est possible pour un Structured Binding de décomposer un struct ou un class. La décomposition se fait de manière positionnelle, pas par nom, alors mieux vaut faire attention et choisir ses noms avec soin.
Bien... | ...moins bien |
---|---|
|
|
Autre caractéristique des Structured Bindings : elles ne peuvent décomposer que les attributs publics d'un type, et qu'elles doivent décomposer (sans omission) tous les attributs de ce type. Ainsi :
Dans l'exemple à droite, il n'est pas possible de décomposer un Point car au moins un de ses attributs est privé. Le phénomène serait le même si les attributs étaient protégés. |
|
De manière un peu surprenante (à mes yeux), l'exemple à droite est aussi illégal du fait que a et b ne peuvent déconstruire *this, et ce même si à ce point, this->x et this->y sont accessibles. Notez qu'après avoir écrit ceci, j'ai constaté qu'un collègue du WG21, Timur Doumler, avait fait le même constat la veille, et qu'il s'agit probablement d'une défectuosité de C++ 17, donc il se peut que cet étrange comportement soit bientôt chose du passé... en fait, nous en avons discuté mercredi matin à Jacksonville 2018 pour adopter un correctif en tant que DR pour C++ 17 |
|
Enfin, l'exemple à droite est illégal du fait que Point comprend trois attributs alors que le code client utilise deux variables pour fins de déconstruction. Notez que rendre un des attributs privés de le masque pas au sens de ce diagnostic : si, pour un Point donné, b est privé alors que x et y sont publics, il demeure illégal de le décomposer en deux variables. |
|
Les Structured Bindings permettent ausse de décomposer un tableau dont la taille est connue à la compilation.
Ainsi, le code proposé à droite est tout à fait correct. |
|
Celui-ci ne l'est pas; un exemple avec trop de variables ne le serait pas non plus. |
|
La décomposition de tableaux bruts est aussi possible, comme le montre l'exemple à droite... |
|
... et notez que, bien qu'il soit difficile de bien retourner un tableau par référence (donc tel que le compilateur en comprenne la taille) d'une fonction, ce n'est pas impossible, comme en témoignent la fonction g() et son appel dans l'exemple à droite. |
|
Les std::tuple étant une généralisation des std::pair, il n'est pas surprenant que les Structured Bindings fonctionnent dans les deux cas. De fait, l'une des utilisations les plus directes des Structured Bindings est la décomposition de la valeur de retour d'un tel agrégat lors d'un appel de fonction.
Prenant pour exemple le langage Go, il est facile d'imaginer une fonction retournant une paire comprenant le succès (ou pas) d'un calcul et, le cas échéant, le résultat de ce calcul, nous pouvons décomposer le résultat pour en arriver à du code client franchement élégant. Ceci peut constituer une alternative aux exceptions, pour qui préfère ne pas utiliser ce mécanisme. |
|
Reprenant l'exemple de la classe Personne donné plus haut, mais remplaçant std::tie() et les variables temporaires dont son appel dépendait par des Structured Bindings, nous en arrivons à du code plus efficace, au sens où le même résultat est obtenu sans imposer le recours à la construction par défaut de variables destinées à être passées à std::tie(). Équivalent à std::ignore? Au moment d'écrire ces lignes, les Structured Bindings n'offrent pas d'équivalent à std::ignore lors d'un appel à std::tie(). Ainsi, il faut nommer chaque variable d'un Structured Binding, que l'on compte l'utiliser ou pas, et chaque nom doit être différent de tous les autres noms de la même portée. |
|
Une application amusante des Structured Bindings est l'initialisation de plusieurs variables de types distincts dans une répétitive for.
Prenons un exemple semi-artificiel (mais piqué à Richard Hodges), soit celui d'une répétitive où l'on souhaite afficher à la fois la valeur de chaque élément d'un conteneur et son indice. Ici, la variable i a été déclarée hors de la répétitive du fait qu'il n'y a pas de moyen simple avec C++ 17 de déclarer plusieurs variables de types distincts dans le bloc d'initialisation d'une même répétitive for. |
|
Il est possible de réaliser une meilleure localité pour les variables en combinant un std::tuple et des Structured Bindings. Depuis l'avènement de CTAD, il est même possible de remplacer ceci :
... par cela :
|
|
Quelques liens pour enrichir le propos.