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 szablonowej klasie bazowej, 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 miksinów — wielokrotnie wykorzystywanej funkcjonalności, którą można „wmieszać” w klasy. W przeciwieństwie do tradycyjnego dziedziczenia miksery 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 podczas kompilacji, co eliminuje narzut związany z funkcjami wirtualnymi. Możesz łączyć wiele mikserów CRTP, aby komponować funkcjonalność, co czyni je elastyczną alternatywą dla polimorfizmu w czasie wykonywania, gdy typy są znane podczas kompilacji.
Wyzwanie
ŁatwyZbudujmy system logowania, który wykorzystuje miksy CRTP do dodawania wielokrotnego użytku funkcjonalności do różnych klas bez narzutu związanego z funkcjami wirtualnymi. Utworzysz dwa miksy, które można „włączyć” do dowolnej klasy: jeden do zliczania instancji, a drugi do generowania reprezentacji tekstowych.
Rozmieścisz kod w trzech plikach:
Mixins.h: Zdefiniuj dwa szablony miksów CRTP, które udostępniają funkcjonalność wielokrotnego użytku.Utwórz szablon miksu
Countable, który śledzi liczbę istniejących instancji klasy pochodnej. Powinien mieć statyczny licznik, który jest zwiększany w konstruktorze i zmniejszany w destruktorze. Udostępnij statyczną metodęgetCount(), która zwraca bieżącą liczbę.Utwórz szablon miksu
Describable, który udostępnia metodędescribe(). Metoda ta powinna używaćstatic_cast, aby uzyskać dostęp do klasy pochodnej i wywołać jej metodęgetDescription(), a następnie wypisać wynik i znak nowej linii.Pamiętaj, że miksy CRTP używają
static_cast<Derived*>(this), aby uzyskać dostęp do składowych klasy pochodnej w czasie kompilacji.Entities.h: Zdefiniuj dwie klasy encji, które dziedziczą po obu miksach.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 tekst w formacie:Player: [name] (Level [level])Utwórz klasę
Enemy, która również dziedziczy po obu miksach. Powinna przechowywać typ i poziom 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żde w osobnym wierszu):- Nazwa gracza (ciąg znaków)
- Poziom gracza (liczba całkowita)
- Typ przeciwnika (ciąg znaków)
- Poziom zdrowia przeciwnika (liczba całkowita)
Utwórz obiekt Player i obiekt Enemy z podanymi wartościami. Następnie zademonstruj działanie miksów:
- Wypisz
Player count: [count]za pomocą 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ą liczbę - 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 miksy 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 dzięki dziedziczeniu po tych samych szablonach miksów, ale każda z nich ma własny, osobny 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 podanej nazwie 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 nazwie "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