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?

Raffinement

En 2018, quelques textes de Jonathan Boccara (voir plus bas pour des liens) ont mis en relief qu'il est possible d'en arriver au même résultat, mais de manière bien plus simple :

using namespace std;
namespace relation {
   template <class T> struct ordonnancement {
      constexpr friend bool operator>(const T &a, const T &b) {
         return b < a;
      }
      constexpr friend bool operator<=(const T &a, const T &b) {
         return !(b < a);
      }
      constexpr friend bool operator>=(const T &a, const T &b) {
         return !(a < b);
      }
   };
   template <class T> struct equivalence {
      constexpr friend bool operator!=(const T &a, const T &b) {
         return !(a == b);
      }
   };
}
namespace manuel {
   class entier : relation::equivalence<entier>, relation::ordonnancement<entier> {
      int val{};
   public:
      entier() = default;
      constexpr entier(int val) : val{ val } {
      }
      constexpr int valeur() const noexcept {
         return val;
      }
      constexpr bool operator==(const entier &autre) const noexcept {
         return valeur() == autre.valeur();
      }
      constexpr bool operator<(const entier &autre) const noexcept {
         return valeur() < autre.valeur();
      }
   };
}
namespace boccara {
   template <class T, template <class> class ... P> struct VCRTP : P<T>... {};
   class entier : VCRTP<entier, relation::equivalence, relation::ordonnancement> {
      int val{};
   public:
      entier() = default;
      constexpr entier(int val) : val{ val } {
      }
      constexpr int valeur() const noexcept {
         return val;
      }
      constexpr bool operator==(const entier &autre) const noexcept {
         return valeur() == autre.valeur();
      }
      constexpr bool operator<(const entier &autre) const noexcept {
         return valeur() < autre.valeur();
      }
   };
}

int main() {
   {
      using manuel::entier;
      static_assert(entier{ 0 } != entier{ 1 });
      static_assert(entier{ 0 } <= entier{ 1 });
   }
   {
      using boccara::entier;
      static_assert(entier{ 0 } != entier{ 1 });
      static_assert(entier{ 0 } <= entier{ 1 });
   }
}

Encore plus charmant, du moins selon moi.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !