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 es Basisklassen, ohne virtuelle Funktionen auf Member der abgeleiteten Klasse zuzugreifen.
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 zum Erstellen von Mixins – wiederverwendbare Funktionalität, die in Klassen „eingemischt“ werden kann. Im Gegensatz zur herkömmlichen Vererbung fügen Mixins Fähigkeiten hinzu, ohne tiefe Hierarchien zu erzeugen:
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. Du kannst mehrere CRTP-Mixins kombinieren, um Funktionalität zusammenzusetzen, was eine flexible Alternative zum Laufzeitpolymorphismus darstellt, wenn die Typen zur Kompilierzeit bekannt sind.
Aufgabe
EinfachErstellen wir ein Protokollierungssystem, das CRTP-Mixins verwendet, um verschiedenen Klassen wiederverwendbare Funktionalität hinzuzufügen, ohne den Overhead virtueller Funktionen. Du erstellst zwei Mixins, die in jede Klasse „eingemischt“ werden können: eines zum Zählen von Instanzen und eines zum Erzeugen von Zeichenfolgendarstellungen.
Du organisierst deinen Code über drei Dateien:
Mixins.h: Definiere zwei CRTP-Mixin-Templates, die wiederverwendbare Funktionalität bereitstellen.Erstelle ein
Countable-Mixin-Template, das verfolgt, wie viele Instanzen einer abgeleiteten Klasse existieren. Es sollte einen statischen Zähler besitzen, 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 deren MethodegetDescription()aufzurufen, und anschließend das Ergebnis gefolgt von einem Zeilenumbruch ausgeben.Denke daran, dass CRTP-Mixins
static_cast<Derived*>(this)verwenden, um zur Compile-Zeit 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 FormatPlayer: [name] (Level [level])zurückgibt.Erstelle eine
Enemy-Klasse, die ebenfalls von beiden Mixins erbt. Sie sollte einen Typ und Lebenspunkte 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 eigenen Zeile):- Player-Name (Zeichenfolge)
- Player-Level (Ganzzahl)
- Enemy-Typ (Zeichenfolge)
- Enemy-Lebenspunkte (Ganzzahl)
Erstelle mit den angegebenen Werten einen Player und einen Enemy. Demonstriere anschließend die Mixins:
- 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 dem Level 1
- Gib erneut
Player count: [count]aus, um den aktualisierten Zählerstand zu zeigen - Rufe
describe()für den zweiten Player auf
Beispielsweise bei 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 durch die Vererbung von denselben Mixin-Templates dieselben Fähigkeiten, aber jede Klasse verwaltet 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 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 & 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