Mixins via CRTP
Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy — leçon 77 sur 104.
Le Curiously Recurring Template Pattern (CRTP) est une technique par laquelle une classe hérite d'une classe de base template, en se passant elle-même comme argument de template. Cela permet un polymorphisme à la compilation et autorise les classes de base à accéder aux membres de la classe dérivée sans utiliser de fonctions virtuelles.
La structure de base du CRTP ressemble à ceci :
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyClass : public Base<MyClass> {
public:
void implementation() {
std::cout << "MyClass implementation\n";
}
};Le CRTP est particulièrement puissant pour créer des mixins - des fonctionnalités réutilisables qui peuvent être « mélangées » à des classes. Contrairement à l'héritage traditionnel, les mixins ajoutent des capacités sans créer de hiérarchies profondes :
template <typename Derived>
class Printable {
public:
void print() const {
const Derived& self = static_cast<const Derived&>(*this);
std::cout << self.toString() << "\n";
}
};
class Person : public Printable<Person> {
std::string name;
public:
Person(const std::string& n) : name(n) {}
std::string toString() const { return "Person: " + name; }
};
// Utilisation :
Person p("Alice");
p.print(); // Résultat : Person: AliceL'avantage principal est que tous les appels de méthodes sont résolus à la compilation, éliminant le surcoût des fonctions virtuelles. Vous pouvez combiner plusieurs mixins CRTP pour composer des fonctionnalités, ce qui en fait une alternative flexible au polymorphisme à l'exécution lorsque les types sont connus à la compilation.
Défi
FacileConstruisons un système de journalisation qui utilise des mixins CRTP pour ajouter des fonctionnalités réutilisables à différentes classes sans le surcoût des fonctions virtuelles. Vous allez créer deux mixins qui peuvent être "mélangés" à n'importe quelle classe : un pour compter les instances et un pour générer des représentations sous forme de chaîne de caractères.
Vous organiserez votre code sur trois fichiers :
Mixins.h: Définissez deux modèles de mixin CRTP qui fournissent des fonctionnalités réutilisables.Créez un modèle de mixin
Countablequi suit le nombre d'instances d'une classe dérivée existantes. Il doit avoir un compteur statique qui s'incrémente dans le constructeur et se décrémente dans le destructeur. Fournissez une méthode statiquegetCount()qui renvoie le compte actuel.Créez un modèle de mixin
Describablequi fournit une méthodedescribe(). Cette méthode doit utiliserstatic_castpour accéder à la classe dérivée et appeler sa méthodegetDescription(), puis afficher le résultat suivi d'un saut de ligne.N'oubliez pas que les mixins CRTP utilisent
static_cast<Derived*>(this)pour accéder aux membres de la classe dérivée au moment de la compilation.Entities.h: Définissez deux classes d'entités qui héritent des deux mixins.Créez une classe
Playerqui hérite à la fois deCountable<Player>et deDescribable<Player>. Elle doit stocker un nom et un niveau, et fournir une méthodegetDescription()qui renvoie une chaîne au format :Player: [name] (Level [level])Créez une classe
Enemyqui hérite également des deux mixins. Elle doit stocker un type et une santé, et fournir une méthodegetDescription()qui renvoie :Enemy: [type] with [health] HPN'oubliez pas d'initialiser le compteur statique pour chaque classe.
main.cpp: Lisez quatre entrées (chacune sur une ligne séparée) :- Nom du joueur (chaîne de caractères)
- Niveau du joueur (entier)
- Type d'ennemi (chaîne de caractères)
- Santé de l'ennemi (entier)
Créez un Player et un Enemy avec les valeurs données. Ensuite, faites la démonstration des mixins :
- Affichez
Player count: [count]en utilisant la méthode statiquegetCount() - Affichez
Enemy count: [count] - Appelez
describe()sur le joueur - Appelez
describe()sur l'ennemi - Créez un second joueur avec le nom "Guest" et le niveau 1
- Affichez à nouveau
Player count: [count]pour montrer le compte mis à jour - Appelez
describe()sur le second joueur
Par exemple, avec les entrées Hero, 10, Dragon, et 500 :
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)Ce défi démontre comment les mixins CRTP ajoutent des fonctionnalités (comptage et description) à des classes non liées sans utiliser de fonctions virtuelles. Player et Enemy acquièrent tous deux les mêmes capacités en héritant des mêmes modèles de mixin, mais chacun conserve son propre compteur d'instances distinct car le modèle est instancié avec des types différents.
Aide-mémoire
Le Curiously Recurring Template Pattern (CRTP) permet le polymorphisme à la compilation en faisant hériter une classe d'une classe de base template, en se passant elle-même comme argument de template :
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyClass : public Base<MyClass> {
public:
void implementation() {
std::cout << "MyClass implementation\n";
}
};Les Mixins CRTP fournissent des fonctionnalités réutilisables sans hiérarchies d'héritage profondes :
template <typename Derived>
class Printable {
public:
void print() const {
const Derived& self = static_cast<const Derived&>(*this);
std::cout << self.toString() << "\n";
}
};
class Person : public Printable<Person> {
std::string name;
public:
Person(const std::string& n) : name(n) {}
std::string toString() const { return "Person: " + name; }
};Avantages clés :
- Tous les appels de méthodes sont résolus à la compilation
- Pas de surcoût lié aux fonctions virtuelles
- Plusieurs mixins peuvent être combinés pour composer des fonctionnalités
- Chaque instanciation de template conserve un état distinct (par exemple, des compteurs statiques séparés par type dérivé)
Essayez vous-même
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Lire les entrées
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Créer un Player avec le nom et le niveau donnés
// TODO: Créer un Enemy avec le type et la santé donnés
// TODO: Afficher "Player count: [count]" en utilisant Player::getCount()
// TODO: Afficher "Enemy count: [count]" en utilisant Enemy::getCount()
// TODO: Appeler describe() sur le joueur
// TODO: Appeler describe() sur l'ennemi
// TODO: Créer un second joueur avec le nom "Guest" et le niveau 1
// TODO: Afficher "Player count: [count]" à nouveau
// TODO: Appeler describe() sur le second joueur
return 0;
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Programmation Orientée Objet
1Fondamentaux de la POO
Fichiers externesBuild et Compilation C++Fichiers d'en-tête et Fichiers sourceEspaces de noms et PortéeIntroduction à la POO en C++Classes vs ObjetsLe pointeur 'this'Méthodes (Fonctions membres)Attributs (Membres de données)Bases des Ctors et DtorsRécapitulatif - Calculatrice simple4Propriétés de classe
Membres d'instance vs membres statiquesGetters et SettersFonctions membres constMot-clé mutableMéthodes et variables statiquesFonctions et classes amiesRécapitulatif - Gestionnaire de compte bancaire7Héritage
Héritage de baseNiveaux d'accès à l'héritageOrdre d'appel des Ctor et DtorRedéfinition de méthodesFonctions virtuelles et VTableHéritage multipleHéritage virtuelRécapitulatif - Hiérarchie des employés10Présentation de la STL
Présentation et philosophie de la STLConteneurs de la STLItérateursAlgorithmes de la STLFoncteurs et expressions lambdaRécapitulatif - Fréquence des mots13Patrons de conception, Partie 1
Introduction aux patrons de conceptionPatron SingletonFabrique & Fabrique abstraitePatron MonteurPatron ObservateurPatron Stratégie2Gestion de la mémoire
Mémoire Pile vs TasPointeurs et RéférencesMémoire dynamique (new/delete)Smart Pointers en C++RAII en C++Récapitulatif - Gestionnaire de tableaux dynamiques5Encapsulation
Spécificateurs d'accès en C++Spécificateurs d'accès en profondeurMasquage d'informationStruct vs ClassClasses imbriquées et internesRécapitulatif - Système de gestion d'étudiants8Polymorphisme
Polymorphisme : Compilation vs ExécutionSurcharge de fonctionsRetour sur les fonctions virtuellesFonctions virtuelles puresClasses abstraitesConception d'interfaces en C++Dynamic Casting & RTTIRécapitulatif - Calculateur de formes11Concepts avancés de la POO
Composition vs HéritageMixins via CRTPIdiome PimplEffacement de typeEnum Classes & Typage fortGestion des exceptions en POOHiérarchies d'exceptions personnalisées14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeRAII en tant que patron3Constructeurs et Destructeurs
Constructeur par défautConstructeur paramétréConstructeur de copieConstructeur de déplacementListes d'initialisation du constructeurConstructeurs déléguésAnalyse approfondie du destructeurRègle de trois / cinq / zéroRécapitulatif - Classe String6Surcharge d'opérateurs
Introduction à la surcharge d'opérateursSurcharge des opérateurs arithmétiquesSurcharge des opérateurs de comparaisonOpérateurs de fluxSurcharge de l'opérateur d'affectationSurcharge des opérateurs [] et ()Opérateurs de conversion de typeRécapitulatif - Classe Matrix9Templates
Templates de fonctionsTemplates de classesSpécialisation de templatesTemplates variadiquesBases de SFINAE & Type TraitsRécapitulatif - Conteneur générique