1. Klassentemplate
Wenn Sie bisher ruhig gelebt haben, indem Sie Task, User, std::vector<Task> und ein paar Funktionen drumherum erstellt haben — gratuliere, Sie sind schon Programmierer: Sie haben gelernt, dasselbe auf verschiedene Weisen zu wiederholen. Jetzt lernen wir, dasselbe auf eine Weise zu wiederholen. Ein Klassentemplate braucht man, wenn man das Gefühl hat: „Ich schreibe dieselbe Klasse, nur ändert sich der Typ im Inneren“.
Stellen wir uns vor, wir entwickeln unsere Konsolenanwendung TaskPilot (ein Mini‑Aufgabenplaner). Wir haben Aufgaben, Projekte, Notizen, sogar „Erinnerungen, Wasser zu trinken“ — und bei allen ähnliche Operationen: Liste speichern, hinzufügen, suchen, ausgeben.
Wenn Sie sich noch nicht sicher sind, ob Sie das brauchen — das ist normal. Eine wichtige Fähigkeit ist: nicht nur zu verstehen, „wie man ein Template schreibt“, sondern auch „wann es wirklich angebracht ist“. Ein Klassentemplate gibt Power, verlangt aber im Gegenzug Sorgfalt.
Das Klassentemplate als Bauplan
Es ist leicht, im Denken zu irren und zu meinen, Box (oder Repository) sei bereits ein fertiger Typ. Tatsächlich ist Box ohne <...> wie das Wort „Tasse“ ohne Präzisierung: Keramik? Pappe? Mit Katze? Der Compiler braucht Details. Ein Klassentemplate ist eine Vorlage, und ein konkreter Typ entsteht erst nach Einsetzen des Template‑Arguments.
Lassen Sie uns die Terminologie festhalten, ohne pedantisch zu werden.
| Begriff | Wie es aussieht | Was das in normalen Worten bedeutet |
|---|---|---|
| Template‑Parameter | |
„Im Inneren gibt es einen unbekannten Typ T, wir ersetzen ihn später“ |
| Template‑Argument | |
„Wir haben den konkreten Typ int anstelle von T eingesetzt“ |
| Instanziierung | |
„Der Compiler hat eine konkrete Version der Klasse für int erzeugt“ |
Kleine Skizze, damit das Gehirn nicht überhitzt:
flowchart TD
A["template <typename T> class Box { ... }"] --> B["Box<int>"]
A --> C["Box<<std::string>"]
A --> D["Box<double>"]
B --> E["Objekt: Box<int> x;"]
C --> F["Objekt: Box<std::string> s;"]
Box<int> und Box<std::string> — das sind verschiedene Typen, genauso wie int und std::string. Sie ähneln sich in der Form, sind aber inkompatibel.
2. Beispiel: Box<T> und Setting<T>
Box<T>: „Box für einen Wert”
Um nicht gleich mit komplexer Architektur zu starten, beginnen wir mit einem spielerischen, aber nützlichen Beispiel — Box<T>. Das ist eine Klasse, die einen einzelnen Wert vom Typ T speichert. Das Beispiel wirkt vielleicht „zu simpel“, zeigt aber sehr gut die Mechanik: wo man template hinsetzt, wo <T> steht, und was const-Methoden bedeuten.
Angenommen, in TaskPilot wollen wir temporär das „aktuell ausgewählte Projekt“ (String) und den „aktuellen Prioritätenfilter“ (Zahl) speichern. Wir können eine Box für jeden Typ machen.
#include <string>
template <typename T>
class Box {
public:
Box() = default;
private:
T value_{};
};
Das ist im Moment fast nutzlos: der Wert ist verborgen, es gibt keine Methoden. Aber der Kern ist sichtbar: T value_{}; — ein Feld, dessen Typ von T abhängt.
Fügen wir ein minimales Interface hinzu: reinlegen und herausnehmen. Und wenden sofort Wissen aus früheren Tagen an: zum Lesen ist oft ein const T& praktisch, um große Objekte nicht zu kopieren.
template <typename T>
class Box {
public:
explicit Box(T value) : value_(value) {}
const T& get() const { return value_; }
void set(T value) { value_ = value; }
private:
T value_{};
};
Nun sieht das in main() so aus:
#include <iostream>
#include <string>
int main() {
Box<int> priority{2};
Box<std::string> project{"Study"};
std::cout << priority.get() << '\n'; // 2
std::cout << project.get() << '\n'; // Study
}
Beachten Sie: der Typ wird explizit angegeben — Box<int>, Box<std::string>. Im Gegensatz zu Funktionen, bei denen der Compiler T oft ableiten kann, muss man bei Klassen in der Basisvariante gewöhnlich den Typ schreiben (automatische Typableitung für Klassen behandeln wir später im Kurs, aber nicht heute).
Setting<T>: weniger Copy‑Paste und einheitlicher Verhaltensvertrag
Wenn Leute zum ersten Mal Box<T> sehen, denken sie oft: „Warum das, wenn man einfach int priority; und std::string project; haben kann?“ Das ist eine faire Frage.
Der Nutzen zeigt sich, wenn das templatisierte Objekt ein gemeinsames Verhalten für verschiedene Typen hat. Zum Beispiel wollen wir in TaskPilot eine „Einstellung“ speichern und sie in einheitlichem Format ausgeben: key = value.
Machen wir Setting<T> — eine einzelne Einstellung.
#include <string>
#include <iostream>
template <typename T>
class Setting {
public:
Setting(std::string name, T value) : name_(std::move(name)), value_(value) {}
void print() const {
std::cout << name_ << " = " << value_ << '\n'; // requires operator<< for T
}
private:
std::string name_;
T value_;
};
Verwendung:
int main() {
Setting<int> maxTasks{"max_tasks", 50};
Setting<std::string> username{"user", "Alice"};
maxTasks.print(); // max_tasks = 50
username.print(); // user = Alice
}
Hier entsteht der „Operationsvertrag“, ähnlich wie bei Funktions-Templates: weil wir std::cout << value_ verwenden, muss der Typ T in einen Stream geschrieben werden können.
Das Klassentemplate hat uns einen einheitlichen Code geliefert: statt zwei Klassen IntSetting und StringSetting haben wir eine Setting<T>.
4. Praktisches Beispiel: Repository<T>
Spielereien sind gut, aber lassen Sie uns etwas machen, das eher wie echter Code aussieht. In TaskPilot haben wir Entitäten: Aufgaben, Projekte, Notizen. Oft leben sie in einem std::vector und wir führen Operationen aus: hinzufügen, nach id suchen, ausgeben.
Wenn man getrennt TaskRepository, ProjectRepository, NoteRepository schreibt, merkt man irgendwann, dass die drei Klassen sich... praktisch gar nicht unterscheiden. Hier kommt das Template richtig zum Zug.
Zuerst fixieren wir ein einfaches Task‑Modell (auf das wir in den vorherigen Kurstagen bereits Bezug genommen haben):
#include <string>
struct Task {
int id{};
std::string title;
bool done{};
};
Nun das Template‑Repository. Es hält ein std::vector<T>. Wir nehmen an, dass T ein Feld id vom Typ int hat. Das ist wichtig: ein Template ist nicht „magisch für alle Typen“, sondern „universell für Typen, die den Vertrag erfüllen“.
#include <vector>
template <typename T>
class Repository {
public:
void add(T item) {
items_.push_back(std::move(item));
}
const T* findById(int id) const {
for (const auto& x : items_) {
if (x.id == id) return &x;
}
return nullptr;
}
private:
std::vector<T> items_;
};
Verwendung mit Task:
#include <iostream>
int main() {
Repository<Task> tasks;
tasks.add(Task{1, "Learn templates", false});
tasks.add(Task{2, "Drink water", true});
const Task* t = tasks.findById(2);
if (t) std::cout << t->title << '\n'; // Drink water
}
Wir erhalten einen sehr wichtigen Effekt: ein Code für Speicherung und Suche funktioniert für verschiedene Entitäten. Wenn morgen Folgendes auftaucht:
struct Project {
int id{};
std::string name;
};
dann funktioniert Repository<Project> sofort — ohne die Klasse zu duplizieren.
Praktisches Beispiel: Ausgabe der gefundenen Entität in TaskPilot
Damit das Thema nicht abstrakt bleibt, fügen wir dem Repository eine kleine Funktion zum Ausgeben einer Aufgabe hinzu. Ausgeben ist kein Template‑Thema, hilft aber zu sehen, wie das templatisierte Klassendesign die Anwendung unterstützt.
Machen wir eine normale Funktion zum Ausgeben einer Task:
#include <iostream>
void printTask(const Task& t) {
std::cout << "#" << t.id << " " << t.title;
std::cout << (t.done ? " [done]\n" : " [todo]\n");
}
Jetzt nutzen wir das Ergebnis von findById:
int main() {
Repository<Task> tasks;
tasks.add(Task{1, "Learn templates", false});
if (const Task* t = tasks.findById(1)) {
printTask(*t); // #1 Learn templates [todo]
}
}
Das Klassentemplate kapselt die „typische Speicher‑Mechanik“, während der Business‑Code (die Aufgabenlogik) normal und lesbar bleibt.
5. Was ein Klassentemplate komplizierter macht
Ein Klassentemplate ist wie ein Schweizer Taschenmesser. Sehr nützlich — bis Sie damit eine Konservendose öffnen wollen, indem Sie die Klinge zu sich hinhalten. Komplexität entsteht meist nicht in der Philosophie, sondern an konkreten Stellen: Syntax, Typinkompatibilitäten, Compiler‑Fehlermeldungen.
Verschiedene Spezialisierungen — verschiedene Typen
Das scheint offensichtlich, aber das Gehirn versucht regelmäßig, sich Energie zu sparen, und behandelt sie als gleich.
Repository<Task> tasks;
Repository<Project> projects;
// tasks = projects; // Fehler: unterschiedliche Typen
Das ist kein Bug, das ist ein Feature: der Compiler schützt Sie vor logischen Fehlern.
Der Name des Templates ohne <...> ist kein Typ
Einsteiger schreiben so:
Repository repo; // Fehler: T ist nicht angegeben
Der Compiler kann nicht erraten, was gespeichert werden soll. Man braucht:
Repository<Task> repo;
Ja, das ist wortreich. Aber ehrlich.
Innerhalb des Templates formulieren Sie implizit Anforderungen an T
In Repository<T>::findById haben wir x.id geschrieben. Das bedeutet, der Typ T muss ein .id haben. Wenn Sie versuchen:
Repository<int> bad;
dann „explodiert“ der Compiler beim Versuch, die Methode zu kompilieren, die id benötigt. Das ist das normale Modell: ein Template wird „bei der Verwendung“ kompiliert, nicht „im Vakuum“.
Bevor wir tief in die Fehlersuche bei Templates einsteigen, ist es nützlich, sich beim Schreiben eines Templates gedanklich zu fragen: „Was muss T können?“
Methoden außerhalb der Klasse: die Syntax wird umfangreicher
Wenn Sie Methodendefinitionen aus dem Klassenkörper auslagern (wie bei normalen Klassen), wird die Schreibweise formeller. Ich zeige ein minimales Beispiel an einer kleinen Klasse, damit wir nicht im Code ertrinken.
template <typename T>
class Box {
public:
void set(T value);
private:
T value_{};
};
template <typename T>
void Box<T>::set(T value) {
value_ = value;
}
Hier gibt es sofort zwei zusätzliche „Schwierigkeiten“: man muss template <typename T> wiederholen, und man muss Box<T>:: schreiben. In kleinen Beispielen ist das nur lästig. In großen Projekten diszipliniert es.
6. Typische Fehler bei der Arbeit mit Klassentemplates
Fehler Nr. 1: das Template als fertigen Typ ansehen.
Oft schreibt man Box b; oder Repository repo; und wundert sich, dass der Compiler meckert. Ein Template ist eine Vorlage. Ein Typ entsteht erst nach Einsetzen des Arguments: Box<int>, Repository<Task>.
Fehler Nr. 2: erwarten, dass Box<int> kompatibel mit Box<double> ist, nur weil „beide Box sind“.
Bei Klassentemplates erzeugt jedes Argument einen separaten Typ. Es ist nicht „eine Klasse mit einer Einstellung“, sondern „eine Klasse‑Familie“. Deshalb ist Zuweisung zwischen verschiedenen Spezialisierungen verboten — das schützt vor Logikvermischung.
Fehler Nr. 3: versehentlich eine Operation im Template einbauen, die T nicht unterstützt.
Beispielsweise verwenden wir in Setting<T>::print() operator<<. Für int und std::string ist das in Ordnung, für Ihren eigenen Typ nicht, solange Sie nicht die Ausgabe unterstützen. Templated Code wird erst bei Verwendung kompiliert, daher kann ein Fehler „unerwartet“ auftreten — nicht an der Deklarationsstelle, sondern an der Stelle der Verwendung.
Fehler Nr. 4: einen Zeiger/Referenz auf ein Element des Containers zurückgeben und ihn dann längerfristig speichern.
Im Beispiel findById haben wir einen const T* auf ein Element innerhalb des std::vector zurückgegeben. Das ist praktisch, aber gefährlich, wenn Sie den Zeiger speichern und später add() aufrufen (der Vektor kann Speicher neu allokieren). Eine sichere Gewohnheit für Einsteiger: den gefundenen Zeiger sofort benutzen, „hier und jetzt“, und ihn nicht als langlebige Variable aufbewahren.
Fehler Nr. 5: wegen langer Compiler‑Meldungen in Panik geraten.
Wenn ein Klassentemplate „für einen Typ nicht passt“, sehen die Fehler oft wie ein Wust an Text aus. Das ist der übliche Preis für generischen Code. Die richtige Vorgehensweise ist, die erste aussagekräftige Zeile zu suchen: welche Operation konnte der Compiler für Ihren T nicht ausführen.
GO TO FULL VERSION