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. |
|
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. |
|
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). |
|
Amusant, n'est-ce pas?
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.
Quelques liens pour enrichir le propos.