Menu
Coddy logo textTech

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: Alice

Kluczową 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.

challenge icon

Wyzwanie

Łatwy

Zbudujmy 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_cast do uzyskania dostępu do klasy pochodnej i wywołania jej metody getDescription(), 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 po Countable<Player>, jak i po Describable<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] HP

    Nie zapomnij zainicjalizować statycznego licznika dla każdej klasy.

  • main.cpp: Wczytaj cztery dane wejściowe (każdy w osobnym wierszu):
    1. Nazwa gracza (ciąg znaków)
    2. Poziom gracza (liczba całkowita)
    3. Typ przeciwnika (ciąg znaków)
    4. Punkty zdrowia przeciwnika (liczba całkowita)

    Utwórz obiekt Player i obiekt Enemy z podanymi wartościami. Następnie zademonstruj działanie miksinów:

    1. Wypisz Player count: [count], używając statycznej metody getCount()
    2. Wypisz Enemy count: [count]
    3. Wywołaj describe() dla gracza
    4. Wywołaj describe() dla przeciwnika
    5. Utwórz drugiego gracza o nazwie "Guest" i poziomie 1
    6. Ponownie wypisz Player count: [count], aby pokazać zaktualizowaną wartość licznika
    7. 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;
}
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator C++ online