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: AliceDer 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.
Aufgabe
EinfachLassen 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 MethodegetCount()bereit, die den aktuellen Zählerstand zurückgibt.Erstellen Sie ein
Describable-Mixin-Template, das einedescribe()-Methode bereitstellt. Diese Methode solltestatic_castverwenden, um auf die abgeleitete Klasse zuzugreifen und derengetDescription()-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 vonCountable<Player>als auch vonDescribable<Player>erbt. Sie sollte einen Namen und ein Level speichern und einegetDescription()-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 einegetDescription()-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):- Spielername (String)
- Spielerlevel (Integer)
- Gegnertyp (String)
- Gegnergesundheit (Integer)
Erstellen Sie einen Player und einen Enemy mit den gegebenen Werten. Demonstrieren Sie dann die Mixins:
- Geben Sie
"Player count: "gefolgt vom Ergebnis der statischengetCount()-Methode aus - Geben Sie
"Enemy count: "gefolgt vom Zählerstand aus - Rufen Sie
describe()für den Spieler auf - Rufen Sie
describe()für den Gegner auf - Erstellen Sie einen zweiten Spieler mit dem Namen
"Guest"und Level1 - Geben Sie erneut
"Player count: "aus, um den aktualisierten Zählerstand anzuzeigen - 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;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container