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, une version C++ 11 et une version C++ 17 du même exemple. Plus la version est récente, et plus elle est à la fois simple et rapide, donc mieux vaut la privilégier si votre compilateur est à jour; l'intérêt des versions antérieures est surtout historique.

  Version C++ 17 Version C++ 11 Version C++ 03

Cet exemple utilisera des listes de types, idée brillante d'Andrei Alexandrescu.

Dans la version C++ 03, 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.

Notez que le type maison type_list pourrait être remplacé, pour l'essentiel, par std::tuple.

#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;
#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;
#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)>

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.

J'ai utilisé template <auto> pour la version C++ 17.

template <auto N>
   struct constant : integral_constant<decltype(N), N> {
   };
template <int N>
   struct int_ : integral_constant<int, N> {
   };
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> { };

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 <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...>>
      : constant<Pred<T>::value ||
                 static_contains<Pred, type_list<Q...>>::value>
   {
   };
template <template <class> class Pred>
   struct static_contains<Pred, type_list<>> : false_type {
   };
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 {
   };
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> {
   };

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<type_list<Q...>>::execute(oper);
         }
   };
template <>
   struct static_for_each<type_list<>> {
      template <class Op>
         static void execute(Op) {
         }
   };
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) {
         }
   };
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>();
         }
   };

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 <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;
   };
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;
   };
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;
   };

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 : constant<!Pred<T>::value> {
         };
   };
template <template <class> class Pred>
   struct not_ {
      template <class T>
         struct type : integral_constant<bool, !Pred<T>::value> {
         };
   };
template <template <class> class Pred>
   struct not_ {
      template <class T>
         struct type : integral_constant<!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<constant<N>> : true_type {
   };
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;
      }
};
#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 : constant<A::value + B::value> {
   };
template <class A, class B>
   struct static_somme : int_<A::value + B::value> {
   };
template <class A, class B>
   struct static_somme {
      using type = 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 : constant<A::value * B::value> {
   };
template <class A, class B>
   struct static_produit : int_<A::value * B::value> {
   };
template <class A, class B>
   struct static_produit {
      using type = 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 : constant<(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> {
   };
template <class A, class B>
   struct static_min {
      using type = 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 : constant<(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> {
   };
template <class A, class B>
   struct static_max {
      using type = 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 = 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;
   }
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;
   }
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;
   }

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);
      }
   };
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 &) {
      }
};
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) {
      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;
   }
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;
   }
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;
   }

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 = type_list<int, short, double, char>;
   using liste_B = type_list<constant<2>, constant<3>, constant<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);
}
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);
}

À 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 !