Exemple de métaprogrammation statique

Ce qui suit est un exemple d'application de la métaprogrammation statique avec C++. Pour des explications, voir l'article sur le sujet.

Je présenterai côte à côte une version C++ 03 et une version C++ 11 du même exemple. La version C++ 11 est plus récente et repose sur une solution standardisée à partir de templates variadiques; elle est plus simple et plus rapide, donc mieux vaut la privilégier si votre compilateur est à jour; l'intérêt de la version C++ 03 est surtout historique.

  Version C++ 03 Version C++ 11

Cet exemple utilisera des listes de types, idée brillante d'Andrei Alexandrescu. J'ai complété le code de base avec quelques macros (oui, je sais...), pour alléger l'écriture, de même qu'avec un accesseur simple nommé static_head<TL> permettant d'obtenir aisément le type en tête de la liste de types TL.

En passant par des templates variadiques, la version C++ 11 évite le recours aux macros pour simplifier le code, visibles dans la version C++ 03.

#include <iostream>
#include <type_traits>
using namespace std;
template <class, class>
   struct type_list;
class Vide {};
template <class>
   struct static_head;
template <class T, class Q>
   struct static_head<type_list<T,Q> > {
      using type = T;
   };
#define MAKE_TLIST(T,Q) type_list<T,Q>
#define MAKE_TLIST_0(T) \
   type_list<T,Vide>
#define MAKE_TLIST_1(T0,T1) \
   type_list<T1,MAKE_TLIST_0(T0)>
#define MAKE_TLIST_2(T0,T1,T2) \
   type_list<T2,MAKE_TLIST_1(T0,T1)>
#define MAKE_TLIST_3(T0,T1,T2,T3) \
   type_list<T3,MAKE_TLIST_2(T0,T1,T2)>
#define MAKE_TLIST_4(T0,T1,T2,T3,T4) \
   type_list<T4,MAKE_TLIST_3(T0,T1,T2,T3)>
#include <iostream>
#include <type_traits>
using namespace std;
template <class...>
   struct type_list;
template <class>
   struct static_head;
template <class T, class ... Q>
   struct static_head<type_list<T, Q...>> {
      using type = T;
   };

template <class TL>
   using static_head_t = typename static_head<TL>::type;

Pour convertir une constante entière N en type, et par la suite raisonner sur des listes de tels types, j'emprunterai la technique proposée à droite à Dave Abrahams. Dans la la version C++ 11, les types integral_constant, true_type et false_type sont standards, tout simplement.

template <class T, T val>
   struct integral_constant {
      using type = T;
      static const constexpr T value = val;
   };
template <int N>
   struct int_ : integral_constant<int,N> { };
struct true_type : integral_constant<bool, true> { };
struct false_type : integral_constant<bool, false> { };
template <int N>
   struct int_ : integral_constant<int, N> {
   };

L'algorithme static_contains<TL,Pred>::value sera true seulement s'il existe au moins un type T dans TL pour lequel Pred<T>::value s'avère.

Ici comme dans les autres algorithmes manipulant des listes de types, remarquez le déplacement de la liste de types dans l'ordre des paramètres du template. En effet, les paramètres variadiques doivent apparaître en dernier dans une séquence de paramètres à un template, ce qui nous amène à structurer les algorithmes dans leur déclinaison C++ 11 de manière à ce que tel soit bel et bien le cas.

template <class TL, template <class> class Pred>
   struct static_contains;
template <class T, class Q, template <class> class Pred>
   struct static_contains<type_list<T, Q>, Pred> :
      integral_constant<bool, Pred<T>::value ||
                              static_contains<Q, Pred>::value> {
   };
template <class T, template <class> class Pred>
   struct static_contains<type_list<T, Vide>, Pred>
      : integral_constant<bool, Pred<T>::value> {
   };
template <template <class> class Pred, class TL>
   struct static_contains;
template <template <class> class Pred, class TL>
   using static_contains_t = typename static_contains<Pred, TL>::type;
template <class T, class ... Q, template <class> class Pred>
   struct static_contains<Pred, type_list<T, Q...>>
      : integral_constant<bool, Pred<T>::value ||
                                static_contains<Pred, type_list<Q...>>::value>
   {
   };
template <template <class> class Pred>
   struct static_contains<Pred, type_list<>> : false_type {
   };

L'algorithme static_for_each<TL> décrit ici est particulier, au sens où il chevauche la métaprogrammation et la programmation conventionnelle. En effet, une instance de static_for_each<TL> expose une méthode de classe execute()acceptant en paramètre une opération oper de type Op et l'appliquant sur chaque type T de TL.

template <class TList>
   struct static_for_each;
template <class T, class Q>
   struct static_for_each<type_list<T, Q>> {
      template <class Op>
         static void execute(Op oper) {
            oper.execute<T>();
            static_for_each<Q>::execute(oper);
         }
   };
template <class T>
   struct static_for_each<type_list<T, Vide>> {
      template <class Op>
         static void execute(Op oper) {
            oper.execute<T>();
         }
   };
template <class TList>
   struct static_for_each;
template <class T, class ... Q>
   struct static_for_each<type_list<T, Q...>> {
      template <class Op>
         static void execute(Op oper) {
            oper.execute<T>();
            static_for_each<type_list<Q...>>::execute(oper);
         }
   };
template <>
   struct static_for_each<type_list<>> {
      template <class Op>
         static void execute(Op) {
         }
   };

L'algorithme static_accumulate<TL,F,Init> applique le foncteur statique binaire F à chaque élément de TL, relayant chaque fois en tant que premier paramètre la valeur accumulée jusque là. Initialement, la valeur accumulée est décrite par le type Init.

template <class TList, template <class, class> class F, class Init>
   struct static_accumulate;
template <class T, class Q, template <class, class> class F, class Init>
   struct static_accumulate<type_list<T, Q>, F, Init> {
      using type = typename F<
         T, typename static_accumulate<Q, F, Init>::type
      >::type;
   };
template <class T, template <class, class> class F, class Init>
   struct static_accumulate<type_list<T, Vide>, F, Init> {
      using type = typename F<T, Init>::type;
   };
template <template <class, class> class F, class Init, class TList>
   struct static_accumulate;
template <template <class, class> class F, class Init, class TL>
   using static_accumulate_t = typename static_accumulate<F, Init, TL>::type;
template <template <class, class> class F, class Init, class T, class ... Q>
   struct static_accumulate<F, Init, type_list<T, Q...>> {
      using type =
         typename F<T, static_accumulate_t<F, Init, type_list<Q...>>>::type;
   };
template <template <class, class> class F, class Init, class T>
   struct static_accumulate<F, Init, type_list<T>> {
      using type = typename F<T, Init>::type;
   };

Le foncteur statique not_ appliqué à un prédicat unaire statique Pred expose un type générique type<T> tel que not_<Pred>::type<T>::value s'avère seulement si Pred<T>::value est false.

template <template <class> class Pred>
   struct not_ {
      template <class T>
         struct type : integral_constant<!Pred<T>::value> {
         };
   };
template <template <class> class Pred>
   struct not_ {
      template <class T>
         struct type : integral_constant<bool, !Pred<T>::value> {
         };
   };

Le prédicat statique is_int<T>::value s'avère seulement si T est un int_<N> pour un certain entier statique N.

template <class>
   struct is_int : false_type {
   };
template <int N>
   struct is_int<int_<N>> : true_type {
   };
template <class>
   struct is_int : false_type {
   };
template <int N>
   struct is_int<int_<N>> : true_type {
   };

Toute instance de la classe afficher_nom offre un une méthode executer() générique sur la base d'un type T qui projette sur un flux le nom du type T.

#include <typeinfo>
class afficher_nom {
   ostream &os;
public:
   afficher_nom(ostream &os) : os{os} {
   }
   template <class T>
      void execute() {
         os << typeid(T).name() << endl;
      }
};
#include <typeinfo>
class afficher_nom
{
   ostream &os;
public:
   afficher_nom(ostream &os) : os{ os } {
   }
   template <class T>
      void execute() {
         os << typeid(T).name() << endl;
      }
};

Le foncteur statique binaire static_somme<A,B> expose un type type représentant la somme des valeurs de A et de B.

template <class A, class B>
   struct static_somme {
      using type = int_<A::value + B::value>;
   };
template <class A, class B>
   struct static_somme : int_<A::value + B::value> {
   };

Le foncteur statique binaire static_produit<A,B> expose un type type représentant le produit des valeurs de A et de B.

template <class A, class B>
   struct static_produit {
      using type = int_<A::value * B::value>;
   };
template <class A, class B>
   struct static_produit : int_<A::value * B::value> {
   };

Le foncteur statique binaire static_min<A,B> expose un type type représentant le minimum des valeurs de A et de B.

template <class A, class B>
   struct static_min {
      using type = int_<(A::value < B::value)? A::value :B::value>;
   };
template <class A, class B>
   struct static_min : int_<(A::value < B::value) ? A::value : B::value> {
   };

Le foncteur statique binaire static_max<A,B> expose un type type représentant le maximum des valeurs de A et de B.

template <class A, class B>
   struct static_max {
      using type = int_<(A::value > B::value)? A::value :B::value>;
   };
template <class A, class B>
   struct static_max : int_<(A::value > B::value) ? A::value : B::value> {
   };

La fonction afficher_infos_types_entiers<TL>(os) projette sur le flux os des informations sur les valeurs colligées dans TL.

template <class TList>
   void afficher_infos_types_entiers(ostream &os) {
      using tete = typename
         static_head<TList>::type;
      os << "Somme des valeurs: " << typename static_accumulate<
         TList, static_somme, int_<0>
      >::type::value << endl;
      os << "Produit des valeurs: " << typename static_accumulate<
         TList, static_produit, int_<1>
      >::type::value << endl;
      os << "Plus petite valeur: " << typename static_accumulate<
         TList, static_min, tete
      >::type::value << endl;
      os << "Plus grande valeur: " << typename static_accumulate<
         TList, static_max, tete
      >::type::value << endl;
   }
template <class TList>
   void afficher_infos_types_entiers(ostream &os) {
      using tete = static_head_t<TList>;
      os << "Somme des valeurs: "
         << static_accumulate_t<static_somme, int_<0>, TList>::value << endl;
      os << "Produit des valeurs: "
         << static_accumulate_t<static_produit, int_<1>, TList>::value << endl;
      os << "Plus petite valeur: "
         << static_accumulate_t<static_min, tete,TList>::value << endl;
      os << "Plus grande valeur: "
         << static_accumulate_t<static_max, tete, TList>::value << endl;
   }

Le type static_afficher_infos_types_entiers<TL> expose une méthode de classe execute(os) appliquant afficher_infos_liste_entiers(os) sur TL.

template <class TList>
   struct static_afficher_infos_types_entiers {
      static void execute(ostream &os) {
         afficher_infos_types_entiers<TList>(os);
      }
   };
template <class TList>
   struct static_afficher_infos_types_entiers {
      static void execute(ostream &os) {
         afficher_infos_types_entiers<TList>(os);
      }
   };

Le type static_no_op expose une méthode execute() générique sur la base d'un type T et dont le rôle est... de ne rien faire, donc d'être un simple no-op.

struct static_no_op {
   template <class T>	
      static void execute(const T &) {
      }
};
struct static_no_op {
   template <class T>	
      static void execute(const T &) {
      }
};

La fonction générique afficher_infos<TL>(os) décrit les types de TL sur os. Elle réalise aussi une série d'opérations mathématiques simples sur les types de TL, mais seulement si tous les types de TL sont des instanciations de int_.

template <class TList>
   void afficher_infos(ostream &os) {
      using has_non_int_type = typename
         static_contains<
            TList, typename not_<is_int>::type
         >;
      static_for_each<TList>::execute(afficher_nom{os});
      if (has_non_int_type::value)
         os << "\tImpossible de cumuler cette liste sur des entiers" << endl;
      typename conditional<
         has_non_int_type::value,
         static_no_op,
         static_afficher_infos_types_entiers<TList>
      >::type::execute(os);
      os << endl;
   }
template <class TList>
   void afficher_infos(ostream &os) {
      static constexpr const bool has_non_int_type = static_contains_t<
         typename not_<is_int>::type, TList
      >::value;
      static_for_each<TList>::execute(afficher_nom{ os });
      if (has_non_int_type)
         os << "\tImpossible de cumuler cette liste sur des entiers" << endl;
      conditional_t<
         has_non_int_type, static_no_op, static_afficher_infos_types_entiers<TList>
      >::execute(os);
      os << endl;
   }

Le programme de test démontre que le tout fonctionne avec une liste d'entiers comme avec une liste ne contenant que des simples types primitifs.

int main() {
   using liste_A = MAKE_TLIST_3(int,short,double,char);
   using liste_B = MAKE_TLIST_2(int_<2>, int_<3>, int_<5>);
   afficher_infos<liste_A>(cout);
   afficher_infos<liste_B>(cout);
}
int main() {
   using liste_A = type_list<int, short, double, char>;
   using liste_B = type_list<int_<2>, int_<3>, int_<5>> ;
   afficher_infos<liste_A>(cout);
   afficher_infos<liste_B>(cout);
}

À l'exécution, dans un cas comme dans l'autre, nous aurons :

int
short
double
char
        Impossible de cumuler cette liste sur des entiers

struct int_<2>
struct int_<3>
struct int_<5>
Somme des valeurs: 10
Produit des valeurs: 30
Plus petite valeur: 2
Plus grande valeur: 5

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !