Muster der Template-Metaprogrammierung
Meistere fortgeschrittene Template-Muster: CRTP für statische Polymorphie, Policy-based Design für flexible Komposition, Type Erasure für generische Typen zur Laufzeit — und lerne, wann Templates und wann virtuelle Aufrufe die richtige Wahl sind.
Jenseits einfacher Templates
Templates ermöglichen Entwurfsmuster, für die es in anderen Sprachen keine Entsprechung gibt. Diese Muster nutzen aus, dass Templates zur Kompilierzeit wirken, und erreichen damit statische Polymorphie (ohne den Overhead virtueller Funktionen), kombinierbares Verhalten (Policies) und typsichere Erasure (konkrete Typen hinter einer einheitlichen Schnittstelle verbergen).
Das sind keine Anfängermuster — es sind Werkzeuge für Bibliotheks- und Framework-Autoren. Wer sie versteht, kann die Interna der Standardbibliothek, Boost und moderne C++-Frameworks lesen.
CRTP: Curiously Recurring Template Pattern
Beim CRTP leitet sich eine Klasse von einem Template ab, das mit ihr selbst als Argument instanziiert wird: class Derived : public Base. Die Basisklasse kann Methoden der abgeleiteten Klasse zur Kompilierzeit aufrufen — ganz ohne den Overhead virtueller Aufrufe. Damit lässt sich Verhalten einbringen, statische Polymorphie umsetzen und Mixin-Funktionalität bauen.
#include <iostream>
// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
static inline int count_ = 0;
protected:
Counter() { ++count_; }
~Counter() { --count_; }
Counter(const Counter&) { ++count_; }
public:
static int alive() { return count_; }
};
// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
std::string name;
Dog(std::string n) : name(std::move(n)) {}
};
class Cat : public Counter<Cat> {
public:
std::string name;
Cat(std::string n) : name(std::move(n)) {}
};
// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
double area() const {
// Call derived class method — resolved at compile time
return static_cast<const Derived*>(this)->area_impl();
}
};
class Circle : public Shape<Circle> {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area_impl() const { return 3.14159265 * radius_ * radius_; }
};
class Square : public Shape<Square> {
double side_;
public:
Square(double s) : side_(s) {}
double area_impl() const { return side_ * side_; }
};
int main() {
Dog d1("Rex"), d2("Buddy");
Cat c1("Whiskers");
std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1
Circle circle(5.0);
Square square(4.0);
std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
std::cout << "Square area: " << square.area() << '\n'; // 16.0
}Policy-based Design
Policy-based Design zerlegt eine Klasse in austauschbare Verhaltensbausteine (Policies), die als Template-Parameter übergeben werden. Jede Policy deckt einen Aspekt des Verhaltens ab. Anwender stellen sich genau die Kombination zusammen, die sie brauchen. Bekannt gemacht hat diesen Ansatz Andrei Alexandrescu mit "Modern C++ Design".
#include <iostream>
#include <mutex>
#include <stdexcept>
// Threading policies
struct SingleThreaded {
struct Lock { Lock(SingleThreaded&) {} }; // no-op
};
struct MultiThreaded {
std::mutex mtx;
struct Lock {
std::lock_guard<std::mutex> guard;
Lock(MultiThreaded& host) : guard(host.mtx) {}
};
};
// Bounds checking policies
struct NoBoundsCheck {
static void check(int /*index*/, int /*size*/) {} // no-op
};
struct StrictBoundsCheck {
static void check(int index, int size) {
if (index < 0 || index >= size)
throw std::out_of_range("Index out of bounds");
}
};
// Policy-based container: users choose their combination
template <typename T,
typename ThreadingPolicy = SingleThreaded,
typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
T* data_;
int size_;
public:
SmartArray(int size) : data_(new T[size]{}), size_(size) {}
~SmartArray() { delete[] data_; }
T& operator[](int index) {
typename ThreadingPolicy::Lock lock(*this);
BoundsPolicy::check(index, size_);
return data_[index];
}
int size() const { return size_; }
};
int main() {
// Fast, no overhead: no locking, no bounds checks
SmartArray<int> fast(10);
fast[0] = 42;
// Safe: bounds-checked, single-threaded
SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
safe[0] = 42;
try {
safe[99] = 0; // throws!
} catch (const std::out_of_range& e) {
std::cout << e.what() << '\n';
}
// Thread-safe with bounds checking
SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
robust[5] = 100;
}Type Erasure
Type Erasure verbirgt den konkreten Typ hinter einer einheitlichen Schnittstelle und verbindet so die Flexibilität von Templates mit der Laufzeitpolymorphie virtueller Funktionen. Das Paradebeispiel ist std::function, das jedes Callable aufnehmen kann. Das Muster besteht aus drei Teilen: einem Concept (der abstrakten Schnittstelle), einem Model (einem Template-Wrapper, der die Schnittstelle für beliebige Typen umsetzt) und einer äußeren Handle-Klasse.
#include <iostream>
#include <memory>
#include <string>
// Type-erased "Printable" — can hold any type with a print() method
class Printable {
// Concept: abstract interface
struct Concept {
virtual ~Concept() = default;
virtual void print(std::ostream& os) const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
// Model: wraps any T that supports operator<<
template <typename T>
struct Model : Concept {
T value;
Model(T v) : value(std::move(v)) {}
void print(std::ostream& os) const override { os << value; }
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(value);
}
};
std::unique_ptr<Concept> ptr_;
public:
// Constructor: accepts any printable type
template <typename T>
Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}
// Copy support
Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
Printable& operator=(const Printable& other) {
ptr_ = other.ptr_->clone();
return *this;
}
Printable(Printable&&) = default;
Printable& operator=(Printable&&) = default;
friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
p.ptr_->print(os);
return os;
}
};
int main() {
// Heterogeneous collection — no common base class needed!
std::vector<Printable> items;
items.emplace_back(42);
items.emplace_back(3.14);
items.emplace_back(std::string("hello"));
items.emplace_back('X');
for (const auto& item : items) {
std::cout << item << '\n';
}
// Output: 42, 3.14, hello, X
}Expression Templates (Überblick)
Expression Templates sind eine Technik, bei der arithmetische Ausdrücke auf Objekten einen Baum von Operationen zur Kompilierzeit aufbauen, statt Zwischenergebnisse zu berechnen. Das Endergebnis entsteht dann in einem einzigen Durchlauf, ganz ohne temporäre Objekte.
Mit einer naiven Vector-Klasse etwa erzeugt a + b + c zwei temporäre Vektoren. Mit Expression Templates baut a + b + c ein leichtgewichtiges Ausdrucksobjekt Add auf, das die Elemente bei Bedarf berechnet — ohne Temporaries.
Hochleistungsbibliotheken für lineare Algebra wie Eigen und Blaze setzen auf diese Technik. Der Ausdrucksbaum wird verzögert ausgewertet, und der Compiler kann die entstehende Ein-Durchlauf-Schleife oft vektorisieren.
Expression Templates sind knifflig korrekt umzusetzen und vor allem ein Thema für Bibliotheksautoren. Wenn du Matrix- und Vektorrechnung brauchst, greif lieber zu einer Bibliothek wie Eigen, statt Expression Templates selbst zu schreiben.
Zur Wahl zwischen Templates (statische Polymorphie) und virtuellen Funktionen (dynamische Polymorphie):
Nimm Templates, wenn:
- die Menge der Typen zur Kompilierzeit feststeht
- du Abstraktionen ohne Overhead brauchst (keine vtable, keine Indirektion)
- der Compiler über Typgrenzen hinweg inlinen und optimieren soll
- du mit Wertsemantik arbeiten willst (keine Heap-Allokation)
- Performance entscheidend ist (enge Schleifen, numerischer Code)
Nimm virtuelle Funktionen, wenn:
- die Menge der Typen offen ist und sich erst zur Laufzeit ergibt (Plugins, Benutzereingaben)
- du heterogene Container brauchst (z. B. vector)
- die Größe des Binaries wichtiger ist als Tempo (Templates erzeugen mehr Code)
- du ein stabiles ABI über Bibliotheksgrenzen hinweg willst
- neue Typen hinzukommen sollen, ohne bestehenden Code neu zu kompilieren
Nimm Type Erasure, wenn:
- du die schlichte Schnittstelle virtueller Aufrufe willst, deine Anwender aber nicht zwingen möchtest, von deiner Basisklasse zu erben
- du Wertsemantik mit Laufzeitpolymorphie brauchst (std::function, std::any)
Template-Metaprogrammierung ist mächtig, hat aber ihren Preis:
1. Explodierende Kompilierzeiten — tief verschachtelte Template-Instanziierungen können die Build-Zeiten untragbar machen. Rekursive TMP ist besonders schlimm — greif lieber zu Fold-Ausdrücken und if constexpr.
2. Fehlermeldungen — ohne Concepts erzeugen Template-Fehler seitenweise unverständlichen Text. Schränke Templates immer ein.
3. Debugging — sich im Debugger durch template-lastigen Code zu hangeln, ist eine Qual. Type-Erasure-Wrapper fügen eine Indirektion hinzu, die den tatsächlichen Typ verdeckt.
4. Lesbarkeit — CRTP, Policy-based Design und Expression Templates sind für weniger erfahrene Entwickler schwer zu durchschauen. Halte Name und Absicht des Musters im Code fest.
5. Überkonstruktion — nicht jedes Problem verlangt nach einer Template-Lösung. Eine schlichte virtuelle Funktion oder sogar ein switch kann klarer und völlig ausreichend sein.
- CRTP ermöglicht statische Polymorphie sowie klassenspezifische Zähler und Mixins, indem man von
Baseableitet - Policy-based Design zerlegt Verhalten in Template-Parameter — Anwender stellen genau die Kombination zusammen, die sie brauchen
- Type Erasure verbindet die Flexibilität von Templates mit Laufzeitpolymorphie (z. B.
std::function,std::any) - Expression Templates vermeiden Temporaries in verketteten Operationen — genutzt von Eigen, Blaze und ähnlichen Bibliotheken
- Nimm Templates für bekannte Typen und Abstraktionen ohne Overhead; virtuelle Aufrufe für offene Typmengen und Flexibilität zur Laufzeit
- Vermeide Überkonstruktion — nimm den einfachsten Mechanismus, der deine Anforderungen erfüllt
Quiz — Teste dein Wissen
(20 XP)1. Was erreicht man beim CRTP mit `class Dog : public Counter<Dog>`?
2. Welches Problem löst Type Erasure?
3. Wann solltest du virtuelle Funktionen gegenüber Templates bevorzugen?