Métaprogrammation – exemples

La métaprogrammation est un sujet palpitant mais déstabilisant au premier abord.

D'un langage de programmation à l'autre, le support à ce niveau d'abstraction varie, et là où plusieurs langages permettent de la métaprogrammation surtout dynamique (avec réflexivité, métaclasses et autres mécanismes de ce genre), C++ offre des mécanismes de métaprogrammation axées vers la performance à l'exécution, donc accentuant le travail statique, fait à la compilation.

Certaines manoeuvres de métaprogrammation sont somme toute simples, comme par exemple déterminer si deux types sont en fait un seul et même type, ce qui peut servir à spécialiser certains algorithmes.

Évidemment, bien que cet exemple soit trivial, ne le codez pas vous-mêmes inutilement; le standard offre std::is_same dans <type_traits> qui offre précisément la même fonctionnalité, et sous la même forme.

template <class T, class U>
   struct meme_type
   {
      enum { value = false };
   };
template <class T>
   struct meme_type<T,T>
   {
      enum { value = true };
   };

D'autres manoeuvres ont pour but d'enrichir les capacités d'extension et de spécialisation d'un programme. Par exemple, C++ offre (sur demande, car ce n'est pas gratuit) le Run Time Type Inference, ou RTTI, sur lequel repose entre autres le transtypage par dynamic_cast, et à partir duquel on peut obtenir un nom pour chaque type.

Ces noms ne sont pas garantis portables d'un compilateur à l'autre, ou d'une version à l'autre d'un même compilateur, mais il est possible de les utiliser comme mécanisme « par défaut » de nommage et de spécialiser les noms qui ne nous conviennent pas.

Par exemple, on pourrait spécialiser traits_noms<T> lorsque T est std::string, qui est au fond std::basic_string<char>, pour que le nom retourné soit alors "string" tout simplement).

#include <typeinfo>

template <class T>
   struct traits_noms
   {
      static constexpr const char* nom()
         { return typeid(T).name(); }
   };

Une manière simple et directe de dire si deux types sont en fait un seul et même type est de combiner les outils déjà présentés ici en une fonction projetant le message approprié sur un flux donné.

#include <ostream>
using namespace std;

template <class T, class U>
   void message_de_comparaison_v0(ostream &os)
   {
      if (meme_type<T, U>::value)
         os << traits_noms<T>::nom() << " est le même type que "
            << traits_noms<U>::nom() << endl;
      else
         os << traits_noms<T>::nom() << " n'est pas le même type que "
            << traits_noms<U>::nom() << endl;
   }

Une autre écriture, plus féconde, repose sur une approche inspirée de std::bind1st(), de STL (aujourd'hui déprécié, mais ça reste un chic exemple), qui permet de transformer une opération binaire (deux opérandes) en une opération unaire (un seul opérande) en liant le 1er paramètre à une valeur donnée (ou, ici, à un type donné). On aurait pu faire de même avec le 2e paramètre, évidemment.

Ce faisant, il devient possible de réduire le savoir requis par la fonction évaluant les comparaisons de type. Pour les fins de l'exemple proposé ici, le foncteur statique binaire transformé en foncteur statique unaire est meme_type (voir le programme principal, plus bas, pour un exemple d'utilisation).

template <class T, template <class, class> class Pred>
   struct static_bind1st
   {
      template <class U>
         struct eval
         {
            enum { value = Pred<T, U>::value };
         };
   };
template <class T, class P>
   void message_de_comparaison_v1(ostream &os)
   {
      if (P::eval<T>::value)
         os << traits_noms<T>::nom() << " : pareil" << endl;
      else
         os << traits_noms<T>::nom() << " : pas pareil" << endl;
   }

L'un des très beaux exemples de métaprogrammation est donné par les listes de types, que nous utiliserons un peu plus bas.

//
// Les listes de types d'Alexandrescu
//
class Vide {};
template <class T, class Q>
   struct type_list
   {
      using tete = T;
      using queue = Q;
   };

Un algorithme statique simple pour trouver un type donné dans une liste de types est exprimé à droite.

template <class TList, class T>
   struct est_dans_v0;
template <class T, class Q>
   struct est_dans_v0<type_list<T, Q>, T>
   {
      enum { value = true };
   };
template <class T, class Q, class U>
   struct est_dans_v0<type_list<T, Q>, U>
   {
      enum { value = est_dans_v0<Q, U>::value };
   };
template <class T>
   struct est_dans_v0<Vide, T>
   {
      enum { value = false };
   };

Un algorithme statique équivalent, mais plus flexible car combinant un foncteur statique à titre de prédicat et un algorithme de recherche statique à l'aide d'un prédicat, est présenté à droite. Avec d'autres prédicats statiques, il deviendrait possible d'exprimer des recherches encore plus complexes.

template <class TList, class Pred>
   struct static_find_if;
template <class T, class Q, class Pred>
   struct static_find_if<type_list<T, Q>, Pred>
   {
      enum { value = Pred::eval<T>::value || static_find_if<Q, Pred>::value };
   };
template <class Pred>
   struct static_find_if<Vide, Pred>
   {
      enum { value = false };
   };

template <class TList, class T>
   struct est_dans_v1
   {
      enum { value = static_find_if<TList, static_bind1st<T, meme_type> >::value };
   };

Les deux exemples d'« appels » aux algorithmes statiques de recherche présentés ici montrent que, pour le code client, les deux approches sont syntaxiquement équivalentes. évidemment, puisque le fruit de l'une comme de l'autre est une constante statique, les performances à l'exécution seront précisément les mêmes dans chaque cas.

template <class TList, class T>
   void message_de_recherche_v0(ostream &os)
   {
      if (est_dans_v0<TList, T>::value)
         os << traits_noms<T>::nom() << " est dans la liste" << endl;
      else
         os << traits_noms<T>::nom() << " n'est pas dans la liste" << endl;
   }
template <class TList, class T>
   void message_de_recherche_v1(ostream &os)
   {
      if (est_dans_v1<TList, T>::value)
         os << traits_noms<T>::nom() << " est dans la liste" << endl;
      else
         os << traits_noms<T>::nom() << " n'est pas dans la liste" << endl;
   }

Un exemple académique de code client démontrant le bon fonctionnement de tout cela est présenté à droite.

#include <iostream>
using namespace std;
int main()
{
   message_de_comparaison_v0<char, char>(cout);
   message_de_comparaison_v0<char, signed char>(cout);
   message_de_comparaison_v0<char, unsigned char>(cout);
   typedef static_bind1st<char, meme_type> static_predicate_type;
   message_de_comparaison_v1<char, static_predicate_type>(cout);
   message_de_comparaison_v1<signed char, static_predicate_type>(cout);
   message_de_comparaison_v1<unsigned char, static_predicate_type>(cout);
   using types_caracteres_1_byte = type_list<
      char, type_list<
         signed char, type_list<
            unsigned char, Vide
         >
      >
   >;
   message_de_recherche_v0<types_caracteres_1_byte, char>(cout);
   message_de_recherche_v0<types_caracteres_1_byte, signed char>(cout);
   message_de_recherche_v0<types_caracteres_1_byte, unsigned char>(cout);
   message_de_recherche_v0<types_caracteres_1_byte, wchar_t>(cout);
   message_de_recherche_v1<types_caracteres_1_byte, char>(cout);
   message_de_recherche_v1<types_caracteres_1_byte, signed char>(cout);
   message_de_recherche_v1<types_caracteres_1_byte, unsigned char>(cout);
   message_de_recherche_v1<types_caracteres_1_byte, wchar_t>(cout);
}

En espérant que le tout vous soit utile...


Valid XHTML 1.0 Transitional

CSS Valide !