Klassen-Templates & CTAD
Baue generische Datenstrukturen mit Klassen-Templates. Verstehe Member-Definitionen, Standardargumente und wie dir CTAD aus C++17 erlaubt, Template-Argumente wegzulassen.
Klassen-Templates
Ein Klassen-Template definiert eine Familie von Klassen, die über einen oder mehrere Typen (oder Nicht-Typ-Werte) parametrisiert sind. Die Standardbibliothek baut auf Klassen-Templates auf: std::vector, std::map, std::optional, std::shared_ptr. Schreibst du std::vector, instanziiert der Compiler die gesamte Klasse mit T = int und erzeugt daraus einen konkreten Typ.
Für Klassen-Templates gelten dieselben Regeln wie für Funktions-Templates: Sie müssen in Headern definiert werden, und jeder eindeutige Satz von Template-Argumenten ergibt einen eigenen Typ. Stack und Stack sind völlig unabhängige Typen — zwischen ihnen besteht keinerlei Vererbungsbeziehung.
Ein generischer Stack
Hier ist ein einfacher generischer Stack. Beachte, dass Memberfunktionen, die außerhalb des Klassenrumpfs definiert werden, die Template-Parameterliste wiederholen müssen.
#include <vector>
#include <stdexcept>
#include <iostream>
template <typename T>
class Stack {
private:
std::vector<T> elements_;
public:
void push(const T& value);
void pop();
const T& top() const;
bool empty() const { return elements_.empty(); }
std::size_t size() const { return elements_.size(); }
};
// Member definitions outside the class require the template header
template <typename T>
void Stack<T>::push(const T& value) {
elements_.push_back(value);
}
template <typename T>
void Stack<T>::pop() {
if (elements_.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elements_.pop_back();
}
template <typename T>
const T& Stack<T>::top() const {
if (elements_.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elements_.back();
}
int main() {
Stack<int> intStack;
intStack.push(42);
intStack.push(17);
std::cout << intStack.top() << '\n'; // 17
intStack.pop();
std::cout << intStack.top() << '\n'; // 42
}Mehrere Parameter & Standardwerte
Klassen-Templates können mehrere Template-Parameter und Standard-Template-Argumente haben, genau wie Funktionsparameter Standardwerte haben können. Standardargumente werden von links nach rechts aufgelöst.
#include <vector>
#include <deque>
#include <iostream>
// Container defaults to std::deque<T> — matches std::stack's design
template <typename T, typename Container = std::deque<T>>
class Stack {
private:
Container elements_;
public:
void push(const T& value) { elements_.push_back(value); }
void pop() { elements_.pop_back(); }
const T& top() const { return elements_.back(); }
bool empty() const { return elements_.empty(); }
};
int main() {
Stack<int> s1; // uses deque<int>
Stack<int, std::vector<int>> s2; // uses vector<int>
s1.push(10);
s2.push(20);
std::cout << s1.top() << '\n'; // 10
std::cout << s2.top() << '\n'; // 20
}Klassen-Template-Argumentdeduktion (CTAD)
Vor C++17 musstest du die Template-Argumente beim Erzeugen einer Klassen-Template-Instanz immer angeben: std::pair. CTAD (Class Template Argument Deduction), eingeführt in C++17, lässt den Compiler die Template-Argumente einer Klasse aus den Konstruktorargumenten ableiten — genau wie bei der Argumentdeduktion von Funktions-Templates.
Mit CTAD: std::pair p(1, 3.14); — der Compiler deduziert std::pair. Das funktioniert für std::vector, std::tuple, std::optional, std::array (C++20) und für deine eigenen Klassen-Templates.
CTAD stützt sich auf Deduction Guides — entweder implizite (aus den Konstruktoren erzeugte) oder explizite (selbst definierte). Der Compiler probiert jede Guide durch und wählt die beste Übereinstimmung.
CTAD & Deduction Guides
Selbst definierte Deduction Guides sagen dem Compiler, wie er Konstruktorargumente auf Template-Argumente abbilden soll. Besonders nützlich sind sie, wenn der Typ des Konstruktorarguments nicht unmittelbar dem Template-Parameter entspricht.
#include <iostream>
#include <cstddef>
template <typename T, std::size_t N>
struct Array {
T data[N];
};
// Without a deduction guide, CTAD cannot deduce T and N.
// This explicit deduction guide maps brace-init with N elements
// of the same type T to Array<T, N>.
template <typename First, typename... Rest>
Array(First, Rest...) -> Array<First, 1 + sizeof...(Rest)>;
// Standard library example: std::vector CTAD from iterators
#include <vector>
#include <list>
int main() {
// CTAD with our custom deduction guide
Array arr{1, 2, 3, 4, 5}; // deduces Array<int, 5>
std::cout << arr.data[2] << '\n'; // 3
// Standard library CTAD examples (C++17)
std::pair p(42, 3.14); // pair<int, double>
std::vector v{1, 2, 3}; // vector<int>
std::tuple t(1, 2.0, 'a'); // tuple<int, double, char>
// CTAD from iterator range
std::list<int> lst{10, 20, 30};
std::vector v2(lst.begin(), lst.end()); // vector<int>
}Member-Templates
Die Memberfunktionen eines Klassen-Templates können selbst Templates sein und zusätzliche Template-Parameter einführen. Das nennt man ein Member-Template. Ein typischer Anwendungsfall ist die Zuweisung oder Konstruktion aus einer verwandten, aber anderen Instanziierung.
Zum Beispiel möchtest du vielleicht einem Stack einen Stack zuweisen — da dies aber unabhängige Typen sind, funktioniert die standardmäßige Kopierzuweisung nicht. Ein Member-Template als Zuweisungsoperator löst das Problem:
template <typename T>
class Stack {
public:
template <typename U>
Stack& operator=(const Stack<U>& other);
};Innerhalb dieses Member-Templates darf sich U von T unterscheiden. Der Rumpf kann Elemente aus other kopieren, solange U in T konvertierbar ist. Die Standardbibliothek nutzt dieses Muster ausgiebig — std::shared_ptr lässt sich über einen Member-Template-Konstruktor in std::shared_ptr umwandeln.
Wenn du ein Klassen-Template spezialisierst, ist die Spezialisierung eine völlig neue Klasse — sie erbt keine Member vom primären Template. Du musst jede Memberfunktion und jedes Datenelement neu definieren. Vergisst du einen Member in der Spezialisierung, gibt es einen Kompilierfehler, sobald dieser Member verwendet wird. Außerdem gibt es partielle Spezialisierung nur für Klassen-Templates, nicht für Funktions-Templates. Die Spezialisierung behandeln wir im nächsten Kapitel ausführlich.
- Klassen-Templates definieren Familien von Klassen — jeder eindeutige Satz von Argumenten ergibt einen eigenen Typ
- Außerhalb der Klasse definierte Memberfunktionen müssen den Kopf
templatewiederholen undClassNameverwenden:: - Standard-Template-Argumente wirken von links nach rechts, genau wie Standardwerte von Funktionsparametern
- CTAD (C++17) lässt den Compiler die Template-Argumente einer Klasse aus den Konstruktorargumenten ableiten
- Explizite Deduction Guides bilden Muster von Konstruktorargumenten auf Template-Argumente ab
- Member-Templates ermöglichen Operationen über verschiedene Instanziierungen desselben Klassen-Templates hinweg
Quiz — Teste dein Wissen
(15 XP)1. Gegeben `template <typename T, typename Container = std::deque<T>> class Stack`: Welcher Typ ist `Stack<int>`?
2. Was ist CTAD (C++17)?
3. Was passiert mit den Membern des primären Templates, wenn du ein Klassen-Template spezialisierst?