Menu
Coddy logo textTech

Mixins via CRTP

Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 77 von 104.

Das Curiously Recurring Template Pattern (CRTP) ist eine Technik, bei der eine Klasse von einer Template-Basisklasse erbt und sich selbst als Template-Argument übergibt. Dies ermöglicht Polymorphismus zur Kompilierzeit und erlaubt es Basisklassen, auf Member der abgeleiteten Klasse zuzugreifen, ohne virtuelle Funktionen zu verwenden.

Die grundlegende CRTP-Struktur sieht wie folgt aus:

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 ist besonders leistungsfähig für die Erstellung von Mixins – wiederverwendbare Funktionalität, die in Klassen „eingemischt“ werden kann. Im Gegensatz zur herkömmlichen Vererbung fügen Mixins Funktionen hinzu, ohne tiefe Hierarchien zu erstellen:

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; }
};

// Verwendung:
Person p("Alice");
p.print();  // Ausgabe: Person: Alice

Der entscheidende Vorteil besteht darin, dass alle Methodenaufrufe zur Kompilierzeit aufgelöst werden, wodurch der Overhead virtueller Funktionen entfällt. Sie können mehrere CRTP-Mixins kombinieren, um Funktionalität zusammenzustellen, was es zu einer flexiblen Alternative zum Laufzeit-Polymorphismus macht, wenn die Typen zur Kompilierzeit bekannt sind.

challenge icon

Aufgabe

Einfach

Lassen Sie uns ein Logging-System bauen, das CRTP-Mixins verwendet, um verschiedenen Klassen wiederverwendbare Funktionalität ohne den Overhead virtueller Funktionen hinzuzufügen. Sie werden zwei Mixins erstellen, die in jede beliebige Klasse "eingemischt" werden können: eines zum Zählen von Instanzen und eines zum Erzeugen von String-Repräsentationen.

Sie werden Ihren Code über drei Dateien organisieren:

  • Mixins.h: Definieren Sie zwei CRTP-Mixin-Templates, die wiederverwendbare Funktionalität bereitstellen.

    Erstellen Sie ein Countable-Mixin-Template, das verfolgt, wie viele Instanzen einer abgeleiteten Klasse existieren. Es sollte einen statischen Zähler haben, der im Konstruktor inkrementiert und im Destruktor dekrementiert wird. Stellen Sie eine statische Methode getCount() bereit, die den aktuellen Zählerstand zurückgibt.

    Erstellen Sie ein Describable-Mixin-Template, das eine describe()-Methode bereitstellt. Diese Methode sollte static_cast verwenden, um auf die abgeleitete Klasse zuzugreifen und deren getDescription()-Methode aufzurufen, und dann das Ergebnis gefolgt von einem Zeilenumbruch ausgeben.

    Denken Sie daran, dass CRTP-Mixins static_cast<Derived*>(this) verwenden, um zur Kompilierzeit auf die Member der abgeleiteten Klasse zuzugreifen.

  • Entities.h: Definieren Sie zwei Entitätsklassen, die von beiden Mixins erben.

    Erstellen Sie eine Player-Klasse, die sowohl von Countable<Player> als auch von Describable<Player> erbt. Sie sollte einen Namen und ein Level speichern und eine getDescription()-Methode bereitstellen, die einen String im Format "Player: " + name + " (Level " + std::to_string(level) + ")" zurückgibt.

    Erstellen Sie eine Enemy-Klasse, die ebenfalls von beiden Mixins erbt. Sie sollte einen Typ und Gesundheit speichern und eine getDescription()-Methode bereitstellen, die "Enemy: " + type + " with " + std::to_string(health) + " HP" zurückgibt.

    Vergessen Sie nicht, den statischen Zähler für jede Klasse zu initialisieren.

  • main.cpp: Lesen Sie vier Eingaben ein (jede in einer separaten Zeile):
    1. Spielername (String)
    2. Spielerlevel (Integer)
    3. Gegnertyp (String)
    4. Gegnergesundheit (Integer)

    Erstellen Sie einen Player und einen Enemy mit den gegebenen Werten. Demonstrieren Sie dann die Mixins:

    1. Geben Sie "Player count: " gefolgt vom Ergebnis der statischen getCount()-Methode aus
    2. Geben Sie "Enemy count: " gefolgt vom Zählerstand aus
    3. Rufen Sie describe() für den Spieler auf
    4. Rufen Sie describe() für den Gegner auf
    5. Erstellen Sie einen zweiten Spieler mit dem Namen "Guest" und Level 1
    6. Geben Sie erneut "Player count: " aus, um den aktualisierten Zählerstand anzuzeigen
    7. Rufen Sie describe() für den zweiten Spieler auf

Zum Beispiel mit den Eingaben Hero, 10, Dragon und 500:

Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)

Diese Herausforderung demonstriert, wie CRTP-Mixins Funktionalität (Zählen und Beschreiben) zu nicht verwandten Klassen hinzufügen, ohne virtuelle Funktionen zu verwenden. Sowohl Player als auch Enemy erhalten dieselben Fähigkeiten, indem sie von denselben Mixin-Templates erben, aber jeder behält seinen eigenen separaten Instanzzähler bei, da das Template mit unterschiedlichen Typen instanziiert wird.

Spickzettel

Das Curiously Recurring Template Pattern (CRTP) ermöglicht Polymorphismus zur Kompilierzeit, indem eine Klasse von einer Template-Basisklasse erbt und sich selbst als Template-Argument übergibt:

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-Mixins bieten wiederverwendbare Funktionalität ohne tiefe Vererbungshierarchien:

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; }
};

Wesentliche Vorteile:

  • Alle Methodenaufrufe werden zur Kompilierzeit aufgelöst
  • Kein Overhead durch virtuelle Funktionen
  • Mehrere Mixins können kombiniert werden, um Funktionalität zusammenzustellen
  • Jede Template-Instanziierung behält einen separaten Zustand bei (z. B. separate statische Zähler pro abgeleitetem Typ)

Probier es selbst

#include <iostream>
#include <string>
#include "Entities.h"

using namespace std;

int main() {
    // Eingaben lesen
    string playerName;
    int playerLevel;
    string enemyType;
    int enemyHealth;
    
    cin >> playerName;
    cin >> playerLevel;
    cin >> enemyType;
    cin >> enemyHealth;
    
    // TODO: Erstelle einen Player mit dem angegebenen Namen und Level
    
    // TODO: Erstelle einen Enemy mit dem angegebenen Typ und Gesundheit
    
    // TODO: Gib "Player count: [count]" unter Verwendung von Player::getCount() aus
    
    // TODO: Gib "Enemy count: [count]" unter Verwendung von Enemy::getCount() aus
    
    // TODO: Rufe describe() für den Player auf
    
    // TODO: Rufe describe() für den Enemy auf
    
    // TODO: Erstelle einen zweiten Player mit dem Namen "Guest" und Level 1
    
    // TODO: Gib "Player count: [count]" erneut aus
    
    // TODO: Rufe describe() für den zweiten Player auf
    
    return 0;
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung