Code de grande personne – templates variadiques et idiome CRTP

Avertissement : ce document met en valeur plusieurs particularités de C++ 11, et suppose pour cette raison une familiarité (le nom de l'article en fait foi) avec les templates variadiques et l'idiome CRTP, deux sujets en soi « avancés ». Vous pouvez bien sûr le lire et en tirer profit sans avoir en banque une compréhension fine de ces a priori, mais n'hésitez pas à vous référer aux liens ci-dessus si vous souhaitez avoir des compléments d'information.

Petite gymnastique amusante : pouvons-nous utiliser les templates variadiques pour appliquer l'idiome CRTP plusieurs fois sur une même classe? Décrit autrement, pouvons-nous dériver une même classe T de l'application de plusieurs templates appliqués à T?

À titre de rappel, l'idiome CRTP se réifie en dérivant une classe d'un type générique appliqué à elle-même. Par exemple :

#include <ostream>
#include <typeinfo>
template <class T>
   struct Descriptible {
      friend void decrire(const T &val, std::ostream &os) {
         os << "Valeur: " << val << "; type: " << typeid(T).name() << std::endl;
      }
   };
struct X : Descriptible<X> {
   friend std::ostream& operator<< (std::ostream &os, const X &) {
      return os << "un X quelconque";
   }
};
#include <iostream>
int main() {
   X x;
   decrire(x, std::cout);
}

Un affichage possible à l'exécution de ce programme serait :

Valeur: un X quelconque; type: struct X

Une application typique de cet idiome est l'enrichissement par l'extérieur des opérations qui y sont applicables. Par exemple :

namespace relation {
   template <class T>
      struct equivalence {
         friend bool operator!=(const T &a, const T &b) {
            return !(a == b);
         }
      };
   template <class T>
      struct ordonnancement {
         friend bool operator<=(const T &a, const T &b) {
            return !(b < a);
         }
         friend bool operator>(const T &a, const T &b) {
            return b < a;
         }
         friend bool operator>=(const T &a, const T &b) {
            return !(a < b);
         }
      };
}

Une programme de test serait :

#include <iostream>
template <class T>
   void tester_relops(const T &a, const T &b, std::ostream &os) {
      using std::endl;
      if (a == b)
         os << a << " == " << b << endl;
      if (a != b)
         os << a << " != " << b << endl;
      if (a < b)
         os << a << " < " << b << endl;
      if (a <= b)
         os << a << " <= " << b << endl;
      if (a > b)
         os << a << " > " << b << endl;
      if (a >= b)
         os << a << " >= " << b << endl;
   }
// ...
class entier : relation::equivalence<entier>, relation::ordonnancement<entier> {
   int val;
public:
   entier(int val) : val(val) {}
   bool operator==(const entier &e) const {
      return val == e.val;
   }
   bool operator<(const entier &e) const {
      return val < e.val;
   }
   friend std::ostream& operator<<(std::ostream &os, const entier &e) {
      return os << e.val;
   }
};
using namespace std;
int main() {
   tester_relops(entier{3}, entier{4}, cout);
}

Ici, nous avons explicitement appliqué CRTP deux fois à entier pour gagner l'ensemble des opérateurs relationnels à partir des opérateurs < et ==.

Pouvons-nous le faire de manière variadique? La réponse est oui, et l'écriture est amusante.

Voici comment j'y suis arrivé (il y a peut-être d'autres moyens).

Tout d'abord, j'ai rédigé une classe réalisant l'application de l'idiome CRTP sur un type T, et j'ai nommé cette classe base_applicator. Cette étape représente le cas simple d'une application de P<T> étant donné T. Je ne suis pas convaincu que cette étape soit absolument nécessaire – elle dénote peut-être mon inexpérience avec ce mécanisme, tout simplement – mais cela fonctionne bien et c'est simple à comprendre, du moins pour qui comprend CRTP.

template <class T, template <class> class P>
   class base_applicator : P<T> {
   };

J'ai ensuite rédigé une classe applicator, générique sur la base d'un type T et d'une liste variadique (donc arbitrairement longue) de templates, chacun desquels est applicables individuellement à un type. La classe applicator réalise une expansion de l'application de base_applicator<T,P> pour chaque template P de la liste de templates en question. C'est un peu la magie de la manoeuvre.

template <class T, template <class> class ... P>
   class applicator : base_applicator<T, P>... {
   };

Enfin, ce qui peut paraître simple à ce stade, le type entier peut être dérivé par application répétée de CRTP à travers plusieurs templates distincts.

Je n'ai utilisé que relation::equivalence et relation::ordonnancement pour cet exemple, mais on aurait pu en ajouter d'autres, à loisir (par exemple, le type Descriptible donné en exemple plus haut pourrait s'ajouter à la liste).

class entier : applicator<
  entier,
  relation::equivalence,
  relation::ordonnancement
>
{
   int val;
public:
   entier(int val) : val{val} {
   }
   bool operator==(const entier &e) const {
      return val == e.val;
   }
   bool operator<(const entier &e) const {
      return val < e.val;
   }
   friend std::ostream& operator<<(std::ostream &os, const entier &e) {
      return os << e.val;
   }
};

Amusant, n'est-ce pas?

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !