Mixiny z użyciem CRTP
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 77 z 104.
Curiously Recurring Template Pattern (CRTP) to technika, w której klasa dziedziczy po bazowej klasie szablonowej, przekazując samą siebie jako argument szablonu. Umożliwia to polimorfizm w czasie kompilacji i pozwala klasom bazowym uzyskiwać dostęp do składowych klas pochodnych bez funkcji wirtualnych.
Podstawowa struktura CRTP wygląda tak:
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";
}
};CRTP jest szczególnie przydatny do tworzenia mixinów — wielokrotnie wykorzystywanych funkcjonalności, które można „domieszać” do klas. W przeciwieństwie do tradycyjnego dziedziczenia mixiny dodają możliwości bez tworzenia głębokich hierarchii:
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; }
};
// Użycie:
Person p("Alice");
p.print(); // Wynik: Person: AliceKluczową zaletą jest to, że wszystkie wywołania metod są rozstrzygane w czasie kompilacji, co eliminuje narzut związany z funkcjami wirtualnymi. Możesz łączyć wiele domieszek CRTP, aby komponować funkcjonalności, dzięki czemu jest to elastyczna alternatywa dla polimorfizmu w czasie wykonywania, gdy typy są znane w czasie kompilacji.
Wyzwanie
ŁatwyZbudujmy system logowania, który używa miksinów CRTP do dodawania wielokrotnego użytku funkcjonalności do różnych klas bez narzutu związanego z funkcjami wirtualnymi. Utworzysz dwa miksiny, które można „wmieszać” do dowolnej klasy: jeden do zliczania instancji, a drugi do generowania reprezentacji tekstowych.
Podzielisz kod na trzy pliki:
Mixins.h: Zdefiniuj dwa szablony miksinów CRTP, które udostępniają wielokrotnego użytku funkcjonalność.Utwórz szablon miksinu
Countable, który śledzi liczbę istniejących instancji klasy pochodnej. Powinien mieć statyczny licznik, który zwiększa się w konstruktorze i zmniejsza w destruktorze. Udostępnij statyczną metodęgetCount(), która zwraca bieżącą wartość licznika.Utwórz szablon miksinu
Describable, który udostępnia metodędescribe(). Ta metoda powinna używaćstatic_castdo uzyskania dostępu do klasy pochodnej i wywołania jej metodygetDescription(), a następnie wypisać wynik i znak nowej linii.Pamiętaj, że miksiny CRTP używają
static_cast<Derived*>(this), aby uzyskać dostęp do składowych klasy pochodnej na etapie kompilacji.Entities.h: Zdefiniuj dwie klasy encji, które dziedziczą po obu miksinach.Utwórz klasę
Player, która dziedziczy zarówno poCountable<Player>, jak i poDescribable<Player>. Powinna przechowywać nazwę i poziom oraz udostępniać metodęgetDescription(), która zwraca ciąg znaków w formacie:Player: [name] (Level [level])Utwórz klasę
Enemy, która również dziedziczy po obu miksinach. Powinna przechowywać typ i punkty zdrowia oraz udostępniać metodęgetDescription(), która zwraca:Enemy: [type] with [health] HPNie zapomnij zainicjalizować statycznego licznika dla każdej klasy.
main.cpp: Wczytaj cztery dane wejściowe (każdy w osobnym wierszu):- Nazwa gracza (ciąg znaków)
- Poziom gracza (liczba całkowita)
- Typ przeciwnika (ciąg znaków)
- Punkty zdrowia przeciwnika (liczba całkowita)
Utwórz obiekt Player i obiekt Enemy z podanymi wartościami. Następnie zademonstruj działanie miksinów:
- Wypisz
Player count: [count], używając statycznej metodygetCount() - Wypisz
Enemy count: [count] - Wywołaj
describe()dla gracza - Wywołaj
describe()dla przeciwnika - Utwórz drugiego gracza o nazwie "Guest" i poziomie 1
- Ponownie wypisz
Player count: [count], aby pokazać zaktualizowaną wartość licznika - Wywołaj
describe()dla drugiego gracza
Na przykład dla danych wejściowych Hero, 10, Dragon i 500:
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)To wyzwanie pokazuje, jak miksiny CRTP dodają funkcjonalność (zliczanie i opisywanie) do niezwiązanych ze sobą klas bez używania funkcji wirtualnych. Zarówno Player, jak i Enemy zyskują te same możliwości, dziedzicząc po tych samych szablonach miksinów, ale każda z nich utrzymuje własny, niezależny licznik instancji, ponieważ szablon jest instancjonowany dla różnych typów.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Wczytaj dane wejściowe
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Utwórz obiekt Player o podanym imieniu i poziomie
// TODO: Utwórz obiekt Enemy o podanym typie i liczbie punktów zdrowia
// TODO: Wypisz "Player count: [count]", używając Player::getCount()
// TODO: Wypisz "Enemy count: [count]", używając Enemy::getCount()
// TODO: Wywołaj describe() dla gracza
// TODO: Wywołaj describe() dla przeciwnika
// TODO: Utwórz drugiego gracza o imieniu "Guest" i poziomie 1
// TODO: Ponownie wypisz "Player count: [count]"
// TODO: Wywołaj describe() dla drugiego gracza
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów11Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieMixiny z użyciem CRTPIdiom PimplWymazywanie typówKlasy enum i silne typowanieObsługa wyjątków w OOPWłasne hierarchie wyjątków3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online