Mixins mittels 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 Compile-Zeit und erlaubt Basisklassen den Zugriff auf Member abgeleiteter Klassen, ohne virtuelle Funktionen zu verwenden.
Die grundlegende CRTP-Struktur sieht so 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, wenn es darum geht, Mixins zu erstellen – wiederverwendbare Funktionalität, die in Klassen „eingemischt“ werden kann. Anders als bei der herkömmlichen Vererbung fügen Mixins Fähigkeiten hinzu, ohne tiefe Hierarchien zu schaffen:
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 Compilezeit aufgelöst werden, wodurch der Mehraufwand virtueller Funktionen entfällt. Du kannst mehrere CRTP-Mixins kombinieren, um Funktionalität zusammenzustellen. Das macht sie zu einer flexiblen Alternative zur Laufzeitpolymorphie, wenn die Typen zur Compilezeit bekannt sind.
Aufgabe
EinfachLass uns ein Protokollierungssystem erstellen, das CRTP-Mixins verwendet, um verschiedenen Klassen wiederverwendbare Funktionalität ohne den Overhead virtueller Funktionen hinzuzufügen. Du erstellst zwei Mixins, die in beliebige Klassen „eingemischt“ werden können: eines zum Zählen von Instanzen und eines zum Erzeugen von Zeichenfolgendarstellungen.
Du verteilst deinen Code auf drei Dateien:
Mixins.h: Definiere zwei CRTP-Mixin-Templates, die wiederverwendbare Funktionalität bereitstellen.Erstelle ein
Countable-Mixin-Template, das nachverfolgt, wie viele Instanzen einer abgeleiteten Klasse existieren. Es sollte einen statischen Zähler haben, der im Konstruktor erhöht und im Destruktor verringert wird. Stelle eine statische MethodegetCount()bereit, die den aktuellen Zählerstand zurückgibt.Erstelle ein
Describable-Mixin-Template, das einedescribe()-Methode bereitstellt. Diese Methode solltestatic_castverwenden, um auf die abgeleitete Klasse zuzugreifen und ihregetDescription()-Methode aufzurufen, und anschließend das Ergebnis gefolgt von einem Zeilenumbruch ausgeben.Denk daran, dass CRTP-Mixins
static_cast<Derived*>(this)verwenden, um zur Kompilierzeit auf die Member der abgeleiteten Klasse zuzugreifen.Entities.h: Definiere zwei Entitätsklassen, die von beiden Mixins erben.Erstelle 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 eine Zeichenfolge im folgenden Format zurückgibt:Player: [name] (Level [level])Erstelle eine
Enemy-Klasse, die ebenfalls von beiden Mixins erbt. Sie sollte einen Typ und einen Gesundheitswert speichern und einegetDescription()-Methode bereitstellen, die Folgendes zurückgibt:Enemy: [type] with [health] HPVergiss nicht, den statischen Zähler für jede Klasse zu initialisieren.
main.cpp: Lies vier Eingaben ein (jeweils in einer separaten Zeile):- Player-Name (Zeichenfolge)
- Player-Level (Ganzzahl)
- Enemy-Typ (Zeichenfolge)
- Enemy-Gesundheit (Ganzzahl)
Erstelle einen Player und einen Enemy mit den angegebenen Werten. Führe anschließend die Mixins vor:
- Gib
Player count: [count]mithilfe der statischen MethodegetCount()aus - Gib
Enemy count: [count]aus - Rufe
describe()für den Player auf - Rufe
describe()für den Enemy auf - Erstelle einen zweiten Player mit dem Namen "Guest" und Level 1
- Gib erneut
Player count: [count]aus, um den aktualisierten Zählerstand zu zeigen - Rufe
describe()für den zweiten Player 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 Aufgabe zeigt, wie CRTP-Mixins nicht verwandten Klassen Funktionalität (Zählen und Beschreiben) hinzufügen, ohne virtuelle Funktionen zu verwenden. Sowohl Player als auch Enemy erhalten dieselben Fähigkeiten, indem sie von denselben Mixin-Templates erben. Jede Klasse behält jedoch ihren eigenen separaten Instanzzähler, da das Template mit unterschiedlichen Typen instanziiert wird.
Probier es selbst
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// Eingaben einlesen
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: Einen Player mit dem angegebenen Namen und Level erstellen
// TODO: Einen Enemy mit dem angegebenen Typ und Gesundheitswert erstellen
// TODO: "Player count: [count]" mit Player::getCount() ausgeben
// TODO: "Enemy count: [count]" mit Enemy::getCount() ausgeben
// TODO: describe() für den Spieler aufrufen
// TODO: describe() für den Gegner aufrufen
// TODO: Einen zweiten Spieler mit dem Namen "Guest" und Level 1 erstellen
// TODO: "Player count: [count]" erneut ausgeben
// TODO: describe() für den zweiten Spieler aufrufen
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 & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins mittels CRTPPimpl-IdiomTyplöschungEnum-Klassen & starke TypisierungAusnahmebehandlung in OOPBenutzerdefinierte Ausnahmehierarchien3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler