Mixin tramite CRTP
Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 77 di 104.
Il Curiously Recurring Template Pattern (CRTP) è una tecnica in cui una classe eredita da una classe base modello, passando se stessa come argomento del modello. Questo consente il polimorfismo a tempo di compilazione e permette alle classi base di accedere ai membri delle classi derivate senza funzioni virtuali.
La struttura di base di CRTP è questa:
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";
}
};Il CRTP è particolarmente potente per creare mixins - funzionalità riutilizzabili che possono essere «inserite» nelle classi. A differenza dell’ereditarietà tradizionale, i mixin aggiungono funzionalità senza creare gerarchie profonde:
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; }
};
// Utilizzo:
Person p("Alice");
p.print(); // Output: Person: AliceIl vantaggio principale è che tutte le chiamate ai metodi vengono risolte in fase di compilazione, eliminando l'overhead delle funzioni virtuali. Puoi combinare più mixin CRTP per comporre le funzionalità, rendendolo un'alternativa flessibile al polimorfismo a runtime quando i tipi sono noti in fase di compilazione.
Sfida
FacileCreiamo un sistema di logging che usa i mixin CRTP per aggiungere funzionalità riutilizzabili a classi diverse senza l'overhead delle funzioni virtuali. Creerai due mixin che possono essere «mescolati» in qualsiasi classe: uno per contare le istanze e uno per generare rappresentazioni sotto forma di stringa.
Organizzerai il codice in tre file:
Mixins.h: definisci due template di mixin CRTP che forniscono funzionalità riutilizzabili.Crea un template di mixin
Countableche tiene traccia del numero di istanze di una classe derivata. Dovrebbe avere un contatore statico che viene incrementato nel costruttore e decrementato nel distruttore. Fornisci un metodo staticogetCount()che restituisce il conteggio attuale.Crea un template di mixin
Describableche fornisce un metododescribe(). Questo metodo dovrebbe usarestatic_castper accedere alla classe derivata e chiamarne il metodogetDescription(), quindi stampare il risultato seguito da un carattere di nuova riga.Ricorda che i mixin CRTP usano
static_cast<Derived*>(this)per accedere ai membri della classe derivata in fase di compilazione.Entities.h: definisci due classi entità che ereditano da entrambi i mixin.Crea una classe
Playerche eredita sia daCountable<Player>sia daDescribable<Player>. Dovrebbe memorizzare un nome e un livello, e fornire un metodogetDescription()che restituisce una stringa nel formato:Player: [name] (Level [level])Crea una classe
Enemyche eredita anch'essa da entrambi i mixin. Dovrebbe memorizzare un tipo e la salute, e fornire un metodogetDescription()che restituisce:Enemy: [type] with [health] HPNon dimenticare di inizializzare il contatore statico per ciascuna classe.
main.cpp: leggi quattro input (ciascuno su una riga separata):- Nome del giocatore (stringa)
- Livello del giocatore (intero)
- Tipo di nemico (stringa)
- Salute del nemico (intero)
Crea un Player e un Enemy con i valori forniti. Poi mostra i mixin in azione:
- Stampa
Player count: [count]usando il metodo staticogetCount() - Stampa
Enemy count: [count] - Chiama
describe()sul giocatore - Chiama
describe()sul nemico - Crea un secondo giocatore con nome "Guest" e livello 1
- Stampa di nuovo
Player count: [count]per mostrare il conteggio aggiornato - Chiama
describe()sul secondo giocatore
Per esempio, con gli input Hero, 10, Dragon e 500:
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)Questa sfida mostra come i mixin CRTP aggiungano funzionalità (conteggio e descrizione) a classi non correlate senza usare funzioni virtuali. Sia Player sia Enemy acquisiscono le stesse capacità ereditando dagli stessi template di mixin, ma ciascuno mantiene un proprio contatore delle istanze, perché il template viene istanziato con tipi diversi.
Provalo tu
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Leggi i dati in ingresso
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Crea un Player con il nome e il livello indicati
// TODO: Crea un Enemy con il tipo e la salute indicati
// TODO: Stampa "Player count: [count]" usando Player::getCount()
// TODO: Stampa "Enemy count: [count]" usando Enemy::getCount()
// TODO: Chiama describe() sul giocatore
// TODO: Chiama describe() sul nemico
// TODO: Crea un secondo giocatore con nome "Guest" e livello 1
// TODO: Stampa di nuovo "Player count: [count]"
// TODO: Chiama describe() sul secondo giocatore
return 0;
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti della programmazione orientata agli oggetti
File esterniBuild e compilazione in C++File header e file sorgenteNamespace e ambitoIntroduzione alla programmazione orientata agli oggetti in C++Classi e oggetti a confrontoIl puntatore 'this'Metodi (funzioni membro)Attributi (membri dati)Fondamenti di costruttori e distruttoriRiepilogo - Calcolatrice semplice4Proprietà delle classi
Membri di istanza e staticiGetter e setterFunzioni membro constParola chiave mutableMetodi e variabili staticiFunzioni e classi friendRiepilogo - Gestore di conti bancari7Ereditarietà
Ereditarietà di baseLivelli di accesso nell’ereditarietàOrdine di chiamata di costruttori e distruttoriRidefinizione dei metodiFunzioni virtuali e VTableEreditarietà multiplaEreditarietà virtualeRiepilogo - Gerarchia dei dipendenti2Gestione della memoria
Memoria Stack vs HeapPuntatori e riferimentiMemoria dinamica (new/delete)Puntatori intelligenti in C++RAII in C++Riepilogo - Gestore di array dinamico5Incapsulamento
Specificatori di accesso in C++Specificatori di accesso in dettaglioOccultamento delle informazioniStruct vs classClassi annidate e interneRiepilogo - Sistema di registrazione degli studenti8Polimorfismo
Polimorfismo a compile time e a runtimeOverload delle funzioniFunzioni virtuali: ripassoFunzioni virtuali pureClassi astratteProgettazione delle interfacce in C++Dynamic casting e RTTIRipasso: calcolatrice di forme11Concetti avanzati di OOP
Composizione vs ereditarietàMixin tramite CRTPIdiom PimplType ErasureEnum class e tipizzazione forteGestione delle eccezioni in OOPGerarchie personalizzate di eccezioni14Pattern di progettazione - Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeRAII come pattern3Costruttori e distruttori
Costruttore predefinitoCostruttore con parametriCostruttore di copiaCostruttore di spostamentoListe di inizializzazione dei costruttoriCostruttori delegantiApprofondimento sui distruttoriRegola del Tre / Cinque / ZeroRipasso - classe String6Sovraccarico degli operatori
Introduzione al sovraccarico degli operatoriSovraccarico degli operatori aritmeticiSovraccarico degli operatori di confrontoOperatori di flussoSovraccarico dell'operatore di assegnazioneSovraccarico degli operatori [] e ()Operatori di conversione di tipoRipasso - Classe Matrix9Template
Template di funzioneTemplate di classeSpecializzazione dei templateTemplate variadiciBasi di SFINAE e dei trait di tipoRiepilogo - Contenitore genericoEsercitati da solo: Compilatore C++ online