Composition de fonctions, partie 02

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

Quelques a priori

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.

#ifndef MEME_TYPE_H
#define MEME_TYPE_H
template <class T, class U>
   struct meme_type {
      enum { value = false };
   };
template <class T>
   struct meme_type<T,T> {
      enum { value = true };
   };
#endif
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.

#ifndef FUNCTIONAL_EXTENSION_H
#define FUNCTIONAL_EXTENSION_H
#include <functional>
template <class R>
   struct nullary_function
      : std::unary_function<R, void>
   {
   };
template <class T>
   struct nullary_function_traits {
      using result_type = typename T::result_type;
   };
template <class R>
   struct nullary_function_traits<R (*)()> {
      using result_type = R;
   };
template <class T>
   struct unary_function_traits {
      using argument_type = typename T::argument_type;
      using result_type = typename T::result_type;
   };
template<class R, class A>
   struct unary_function_traits<R (*)(A)> {
      using argument_type = A;
      using result_type = R;
   };
#endif

Enrichir sémantiquement les fonctions composites

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.

#ifndef COMPOSITE_FUNCTION_TRAITS_H
#define COMPOSITE_FUNCTION_TRAITS_H
template <class, class>
   class compose_fx_gx_impl;
template <class, class>
   class fx_then_gx_impl;

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.

template <class>
   struct composite_function_traits;
template <class FX, class GX>
   struct composite_function_traits<
      compose_fx_gx_impl<FX, GX>
   >
   {
      using function_0_type = FX;
      using function_1_type = GX;
   };
template <class FX, class GX>
   struct composite_function_traits<
      fx_then_gx_impl<FX, GX>
   >
   {
      using function_0_type = FX;
      using function_1_type = GX;
   };

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.

template <class>
   struct is_composite_function {
      enum { value = false };
   };
template <class FX, class GX>
   struct is_composite_function<
      compose_fx_gx_impl<FX, GX>
   >
   {
      enum { value = true };
   };
template <class FX, class GX>
   struct is_composite_function<
      fx_then_gx_impl<FX, GX>
   >
   {
      enum { value = true };
   };

Le trait clé de la mécanique statique développée ici est possesses_function<C,F>, dont la constante value sera :

  • true si C est un composite dont l'un des éléments constitutifs est F;
  • true si l'un des éléments constitutifs U de C s'avère posséder F au sens récursif de possesses_function<U,F>::value; et
  • false dans tous les autres cas.

Remarquez la structure algorithmique qui nous permet de réaliser ce calcul :

  • le type interne et privé not_composite_function représente le cas où C n'est pas une fonction composite;
  • le type générique (c'est important!) interne et privé recursive_possesses_function_impl<U> expose la constante value qui ne sera true que si U possède directement la fonction F (remarquez le recours aux traits) ou si au moins un des éléments constitutifs de U possède indirectement (au sens récursif de possesses_function) la fonction F;
  • enfin, le type générique (c'est important!) interne et privé recursive_possesses_function<U> utilise la technique du sélecteur statique de parents :
    • la valeur du trait indiquant si U est composite ou non est examinée;
    • si U est composite, alors l'examen récursif de sa possession (ou non) de F est réalisé, et le parent de notre classe aura la constante de classe value de valeur true seulement si U possède effectivement F;
    • si U n'est pas composite, alors le parent de notre classe sera not_composite_function, dont la constante de classe value sera false;
    • notre classe sera donc une classe vide mais aura, à travers son parent, une constante de classe value dont la valeur ne sera true que si F est possédée, directement ou indirectement, par U;
  • enfin, la constante de classe publique value, qui sera celle consultée par le code client, ne sera true que si C possède, directement ou indirectement, F.

Tout ce travail nous rapportera gros, n'ayez crainte!

#include <type_traits>
template <class C, class F>
   struct possesses_function {
   private:
      struct not_composite_function {
         enum { value = false };
      };
      template <class U>
         struct recursive_possesses_function_impl {
            using traits_U = composite_function_traits<U>;
            enum {
               value = meme_type<
                  typename traits_U::function_0_type, F
               >::value || std::is_same<
                  typename traits_U::function_1_type, F
               >::value || possesses_function<
                  typename traits_U::function_0_type, F
               >::value || possesses_function<
                  typename traits_U::function_1_type, F
               >::value
            };
         };
      template <class U>
         struct recursive_possesses_function
            : std::conditional<
                 is_composite_function<U>::value,
                 recursive_possesses_function_impl<U>,
                 not_composite_function
              >::type
         {
         };
   public:
      enum {
         value = recursive_possesses_function<C>::value
      };
   };

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.

template <class F0, class F1, class T>
   struct composite_function_child_selector {
      using type = typename
         std::conditional<
            possesses_function<F0, T>::value,
            F0, F1
         >::type;
   };
#endif

Parenthèse technique

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.

Les foncteurs composites eux-mêmes

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 :

  • Une assertion statique valide dès la compilation la tentative de conversion
  • Une fois la validité de l'opération déterminée, le relais de la demande to_<T>() est envoyée vers FX ou vers GX en trois temps (suivez bien la manoeuvre, c'est charmant!) :
    • tout d'abord, nous utilisons le trait composite_function_child_selector vu plus haut pour identifier lequel de FX et de GX possède T. Pour les fins de l'explication, nommons ce type type;
    • une fois le type type identifié, nous invoquons notre propre méthode to_<type>(). Ceci invoquera nécessairement soit to_<FX>(), soit to_<GX>(), qui sont toutes deux spécialisées (par définition);
    • l'objet résultant de cette première invocation implémentera nécessairement to_<T>() de manière directe ou, comme nous, indirecte. Nous lui relayons donc cet appel, qu'il implémentera à sa manière.

 

#ifndef COMPOSE_FX_GX_H
#define COMPOSE_FX_GX_H
#include "functional_extension.h"
#include "composite_function_traits.h"
template <class FX, class GX>
   class compose_fx_gx_impl
      : public std::unary_function<
           typename unary_function_traits<GX>::argument_type,
           typename unary_function_traits<FX>::result_type
        >
   {
      FX fx_;
      GX gx_;
   public:
      compose_fx_gx_impl(FX fx, GX gx)
         : fx_{fx}, gx_{gx}
      {
      }
      result_type operator()(argument_type arg)
         { return fx_(gx_(arg)); }
      template <class T>
         T to_() {
            static_assert(
               possesses_function<
                  compose_fx_gx_impl<FX,GX>, T
               >::value,
              "Indirection illegale"
            );
            return to_<
               typename composite_function_child_selector<
                  FX,GX,T
               >::type
            >().to_<T>();
         }
      template <>
         FX to_<FX>()
            { return fx_; }
      template <>
         GX to_<GX>()
            { return gx_; }
   };
template <class FX, class GX>
   compose_fx_gx_impl<FX, GX> compose_fx_gx(FX fx, GX gx)
      { return compose_fx_gx_impl<FX, GX>{fx, gx}; }
#endif
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.

#ifndef FX_THEN_GX_H
#define FX_THEN_GX_H
#include "functional_extension.h"
#include "composite_function_traits.h"
template <class FX, class GX>
   class fx_then_gx_impl
      : public std::unary_function<
           typename unary_function_traits<FX>::argument_type,
           typename unary_function_traits<FX>::result_type
        >
   {
      FX fx_;
      GX gx_;
   public:
      fx_then_gx_impl(FX fx, GX gx)
         : fx_{fx}, gx_{gx}
      {
      }
      result_type operator()(argument_type arg)
         { return fx_(arg), gx_(arg); }
      template <class T>
         T to_() {
            static_assert(
               possesses_function<
                  fx_then_gx_impl<FX,GX>, T
               >::value,
               "Indirection illegale"
            );
            return to_<
               typename composite_function_child_selector<
                  FX,GX,T
               >::type
            >().to_<T>();
         }
      template <>
         FX to_<FX>()
            { return fx_; }
      template <>
         GX to_<GX>()
            { return gx_; }
   };
template <class FX, class GX>
   fx_then_gx_impl<FX, GX> fx_then_gx(FX fx, GX gx)
      { return fx_then_gx_impl<FX, GX>{fx, gx}; }
#endif

Exemple de code client

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.

#include <functional>
template <class T>
   class cumuler : public std::unary_function<T,void> {
   public:
      using value_type = T;
   private:
      value_type val_;
   public:
      cumuler(value_type init = value_type())
         : val_{init}
      {
      }
      value_type valeur() const
         { return val_; }
      result_type operator()(argument_type val)
         { val_+= val; }
   };

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

#include <cmath>
template <class T>
   struct racine_carree : std::unary_function<T, T> {
      result_type operator()(argument_type arg)
         { return std::sqrt(arg); }
   };

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

template <class T>
   struct negate : std::unary_function<T, T> {
      result_type operator()(argument_type arg)
         { return -arg; }
   };

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

template <class T>
   struct compter : std::unary_function<T, T> {
   private:
      int cpt_;
   public:
      compter() : cpt_{} {
      }
      int valeur() const
         { return cpt_; }
      result_type operator()(argument_type arg)
         { ++cpt_; return arg; }
   };

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.

struct bidon {
   int valeur() const
      { return 3; }
};

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.

#include "compose_fx_gx.h"
#include <algorithm>
#include <iostream>
int main() {
   using namespace std;
   int vals[] = { 1, 4, 9, 16, 25 };
   cout << for_each(begin(vals), end(vals),
                    compose_fx_gx(
                       cumul<double>{}, compose_fx_gx(
                          compter<double>(), racine_carree<double>())
                       )
                    ).to_<cumul<double>>().valeur() << endl;
   cout << for_each(begin(vals), end(vals),
                    compose_fx_gx
                       (cumul<double>{}, compose_fx_gx
                          (compter<double>(), racine_carree<double>()))).to_<
                               compter<double>
                           >().valeur() << endl;
//
// L'EXEMPLE CI_DESSOUS NE COMPILE PAS (TANT MIEUX!)
//
/*
   cout << for_each(begin(vals), end(vals),
                    compose_fx_gx
                       (cumul<double>(), compose_fx_gx
                          (compter<double>(), racine_carree<double>()))).to_<
                               bidon
                           >().valeur() << endl;
*/
}

En espérant que le tout vous ait amusé et inspiré!


Valid XHTML 1.0 Transitional

CSS Valide !