Le texte qui suit est pertinent avec C++ 03, mais l'est moins avec C++ 11. Pour un texte plus actuel, voir composition_fonctions.html.
Si vous ne les avez pas encore lues, commencez par la partie 00 de ce document, puis lisez la partie 01.
Nous allons maintenant élever le niveau de sophistication de notre modèle et permettre la manipulation naturelle de classes composites arbitrairement complexes. En plus des documents susmentionnés, comprendre les bases de métaprogrammation vous sera d'une grande utilité pour profiter de celui-ci.
Il y aura un peu de code de grande personne dans la présente. Vous voilà avertis.
Nous allons maintenant examiner comment étendre la fonctionnalité offerte par la méthode d'instance to_<T>() de notre foncteur composite non seulement à ses attributs d'instance, mais aussi à leurs propres version de to_<T>() si les attributs d'instances en question sont eux-aussi des entités composites.
Ceci nous permettra de construire des composites arbitrairement complexes, de les utiliser dans des algorithmes standards de manière efficace, et de tirer d'eux les résultats dont le code client aura besoin. Nous ferons aussi en sorte de nous protéger contre des expressions incorrectes (comme faire compose_fx_gx(f,g).to_<H>() si ni f, ni g, ni leurs éléments constitutifs ne savent se convertir en H).
Commençons par mettre quelques a priori de notre démarche en place. La plupart d'entre eux ont déjà été décrits dans d'autres articles (des hyperliens seront offerts ici et là pour celles et ceux qui souhaitent se rafraîchir la mémoire).
Nous utiliserons des assertions statiques pour générer des erreurs pertinentes lorsque des tentatives illégales de conversion seront entreprises par le code client.
Par exemple, pour que f_g_.to_<T>() soit légal pour f_g_ de type compose_fx_gx_impl<FX,GX>, il faut :
Notez qu'on pourrait remplacer la contrainte demandant que T et FX (ou que T et GX) soient un seul et même type par une exigence que FX soit convertible en T ou que GX soit convertible en T, mais en pratique cela n'est pas strictement nécessaire et peut entraîner des ambiguïtés supplémentaires, un peu à la manière des constructeurs implicites ou des opérateurs de conversion implicites.
meme_type.h | |
Déterminer si deux types sont au fond un seul et même type ou non est l'une des manoeuvres de métaprogrammation les plus simples qui soient. L'illustration à droite montre une manère immédiate d'y arriver. Le code est probablement auto-explicatif. depuis C++ 11, dans <type_traits>, ce concept est implémenté par std::is_same. |
|
functional_extension.h | |
Dans la partie 00 de cette série, nous avons mis en place une série de traits permettant de documenter et d'identifier les caractéristiques à la fois des fonctions et des foncteurs unaires. Le code en question est reproduit ici pour simplifier la lecture, mais devrait vous sembler évident si la technique des traits vous est familière. Au besoin, référez-vous aux explications données dans la partie 00. |
|
Entrons maintenant dans le vif du sujet et enrichissons sémantiquement nos fonctions composites. Pour garder le modèle souple et extensible, nous utiliserons des traits (les classes sont fermées pour ajouts, mais les traits peuvent être ajoutés à loisir pour enrichir les classes et les entités plus primitives; pour construire un modèle extensible, cette technique est d'une valeur inestimable).
Dans le but de démontrer l'extensibilité de notre approche, d'ailleurs, nous définirons deux foncteurs composites :
Évidemment, quiconque le souhaite pourra créer d'autres foncteurs composites en les déposant dans d'autres fichier, en incluant composite_function_traits.h (ci-dessous) et en spécialisant les traits appropriés en fonction de ses propre besoins.
composite_function_traits.h | |
Remarquez tout d'abord la déclaration a priori des deux foncteurs composites qui nous intéressent. À strictement parler, ces déclarations n'ont pas à être ici, mais elles nous permettront de définir tout de suite les traits pertinents pour ces classes. Ces déclarations éviteront des inclusions circulaires lorsque les foncteurs composites voudront utiliser les traits définis ici. |
|
Par souci d'homogénéité, nous définirons des traits documentant les types des fonctions composites qui nous intéressent. Un raffinement possible ici serait de remplacer les types explicitement exprimés par les types internes et publics function_0_type et function_1_type par une liste de types, ce qui permettrait de généraliser la solution à des composites plus vastes. Notre capacité d'exprimer des composites de composites, toutefois, fera en sorte que nous n'aurons pas besoin de ce raffinement. |
|
Il est notoirement difficile (peut-être même impossible; ce n'est pas clair) de définir de manière générale si un type T possède un trait U. Il est par contre possible de définir des indicateurs constants statiques en ce sens si nous le souhaitons. Le trait is_composite_function<F>::value sera true seulement si F prétend être un foncteur composite. Nous définirons ce trait comme étant false de manière générale, et nous le spécialiserons à true pour les cas appropriés. Ajouter des foncteurs composites à un programme demandera de spécialiser cet aiguillage statique pour ces nouveaux foncteurs. |
|
Le trait clé de la mécanique statique développée ici est possesses_function<C,F>, dont la constante value sera :
Remarquez la structure algorithmique qui nous permet de réaliser ce calcul :
Tout ce travail nous rapportera gros, n'ayez crainte! |
|
Enfin, pour clore le portrait, nous définirons une classe générique composite_function_child_selector<F0,F1,T>, dont le type interne et public type correspondra à F0 si F0 possède T et à F1 sinon. Cette manoeuvre nous permettra de faire un peu de magie (plus bas). Nous pourrions rendre ceci encore plus rigoureux en validant que F1 possède bel et bien T, mais dans notre cas cette vérification sera faite ailleurs. Si vous en avez envie, je vous invite à ajouter la couche de vernis manquante. |
|
Pourquoi est-il si important que, dans possesses_function<C,F>, les types internes et privés recursive_possesses_function et recursive_possesses_function_impl soient générique?
La réponse est simple : à cause du principe SFINAE, qui dit (pour paraphraser) que du code non généré mais syntaxtiquement acceptable n'est pas erroné.
Ici, pour possesses_function<C,F>, il est important ne ne générer que des expressions valides à partir de C et de F. Puisque nous cherchons entre autres à savoir si C est composite, nous ne pouvons pas générer du code qui dépende du fait que C soit composite (ce code ne serait pas valide).
La classe interne recursive_possesses_function<U> ne sera générée par le compilateur pour ce U que si le compilateur doit le faire. Ainsi, grâce au sélecteur de parents dans recursive_possesses_function, le parent ne sera généré que si la condition du static_if_else est true, donc seulement si les traits de U (en fait, de C) indiquent que U est une fonction composite.
En guidant la compilation à partir de types génériques, de constantes et d'alternatives statiques, nous pouvons donc guider le compilateur dans la génération des éléments pertinents pour notre programme, en évitant les écueils des éléments qui ne nous conviendraient pas.
Examinons maintenant les fonctions composites eux-mêmes, enrichis des fruits de nos efforts aux parties 00 et 01 de même que de ceux déployés ci-dessus.
compose_fx_gx.h | |
Pour l'essentiel, cette classe est exactement telle qu'elle était dans la partie 01. La véritable nouveauté est que la fonction générique to_<T>(), plutôt que d'être laissée indéfinie, est implémentée de la manière suivante :
|
|
fx_then_gx.h | |
Conséquence de tout cela : au prix de copies d'éléments constitutifs intermédiaires, probablement toutes très légères à réaliser et faciles à optimiser pour le compilateur, nous parcourons récursivement les éléments constitutifs jusqu'au point d'obtention de la véritable instance de T demandée. Un raffinement possible de ce modèle tiendrait compte du potentiel de demandes de conversion ambiguës. Si vous en avez envie, amusez-vous à l'implémenter. À titre d'exemple, voici fx_then_gx() et fx_then_gx_impl, faits à peu près sur le même modèle. Si vous le souhaitez, vous pouvez identifier les éléments communs aux deux implémentations (compose_fx_gx_impl et fx_then_gx_impl) puis refactoriser pour que le tout soit plus élégant. Ceci pourrait être bénéfique si vous envisagez étendre le modèle à d'autres structures composites. |
|
Reste évidemment à montrer que le tout fonctionne.
Principal.cpp | |
Pour nous besoins d'expérimentation, nous utiliserons quelques opérations susceptibles d'être combinées. La première sera décrite par le foncteur générique cumuler, directement inspiré des exemples des parties précédentes. |
|
La seconde sera le foncteur racine_carree. Ceci simplifiera notre travail (utiliser simplement la fonction std::sqrt() dans le code client provoquerait des erreurs car le standard en offre trois versions distinctes). |
|
La troisième sera décrite par le foncteur negate, qui retourne la négation artithmétique de son paramètre. Vous pouvez évidemment aussi utiliser le foncteur équivalent de la bibliothèque standard, ou une simple fonction (probablement générique). |
|
La quatrième sera le foncteur compter, qui n'applique aucun traitement sur ses paramètres (c'est un Pass-Through) mais compte le nombre d'invocations faites sur son opérateur (). Ceci donne un bon exemple d'utilité des compositions génériques et généralisées de fonctions : comptabiliser de manière non intrusive des invocations de méthodes... |
|
Enfin, pour démontrer notre capacité à attraper les tentatives de conversion incongrues, nous utiliserons le type bidon. Nous souhaitons en effet qu'une tentative de conversion to_<bidon>() sur un composite qui ne soit pas en partie fait d'un bidon échoue avec un message d'erreur convenable. |
|
Le programme de test que nous utiliserons sera celui proposé à droite. Il pacourt une séquence d'entiers, calcule la somme des racines carrées et affiche... quelque chose, selon le fruit de l'appel à to_<>(). Le tout est réalisé à l'aide de composites complexes mêlant composites et non composites. Le premier affichera la somme des racines carrées. Le deuxième affichera le nombre d'éléments dans la séquence selon compter. Le troisième, si vous en retirez les commentaires, ne compilera pas, dû à l'assertion statique. |
|
En espérant que le tout vous ait amusé et inspiré!