Zum Inhalt springen

Template-Spezialisierung & Tag Dispatch

Meistere vollständige und partielle Spezialisierung, lerne, wann Überladung die bessere Wahl ist, und entdecke Tag Dispatch sowie if constexpr als Alternativen für Verzweigungen zur Kompilierzeit.

Warum spezialisieren?

Das primäre Template liefert eine generische Implementierung, die für die meisten Typen passt. Manchmal braucht ein bestimmter Typ aber eine grundlegend andere Behandlung. std::vector etwa ist eine Spezialisierung, die bools platzsparend in einzelne Bits packt (auch wenn sie weithin als Fehlentscheidung der Standardbibliothek gilt).

Mit Template-Spezialisierung gibst du eine alternative Definition für bestimmte Template-Argumente an. Es gibt zwei Arten:

1. Vollständige (explizite) Spezialisierung — du legst alle Template-Parameter auf konkrete Typen fest.
2. Partielle Spezialisierung — du legst einige Parameter fest oder schränkst sie ein (z. B. eine Spezialisierung für alle Zeigertypen). Partielle Spezialisierung gibt es nur für Klassen-Templates, nicht für Funktions-Templates.

Vollständige Spezialisierung

Eine vollständige Spezialisierung liefert eine komplett eigene Implementierung für genau einen Satz von Template-Argumenten. Die Syntax verwendet template <> (eine leere Parameterliste), gefolgt von der Klasse oder Funktion mit konkreten Argumenten.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Partielle Spezialisierung

Eine partielle Spezialisierung schränkt einige Template-Parameter ein und lässt die übrigen generisch. Das ist außerordentlich mächtig, um ganze Typfamilien zu treffen — alle Zeiger, alle Arrays, alle Paare und so weiter. Nur Klassen-, Struct- und Variablen-Templates unterstützen partielle Spezialisierung.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Stolperfalle

Funktions-Templates können nicht partiell spezialisiert werden — nur vollständig. Schlimmer noch: Spezialisierungen von Funktions-Templates nehmen nicht so an der Überladungsauflösung teil, wie man es erwarten würde. Der Compiler wählt zuerst per Überladungsauflösung das beste Basis-Template und prüft erst danach, ob es für dieses Basis-Template eine passende Spezialisierung gibt.

Das führt zu überraschendem Verhalten:

template <typename T> void f(T)   { /* #1 - primär */ }
template <typename T> void f(T*)  { /* #2 - Überladung für Zeiger */ }
template <> void f<int*>(int*)     { /* #3 - Spezialisierung von #1 */ }

Der Aufruf f((int*)nullptr) wählt #2 (die besser passende Überladung), nicht #3 (das ja #1 spezialisiert, nicht #2). Die Abhilfe: verwende ganz normale Funktionsüberladung statt Spezialisierung von Funktions-Templates. Brauchst du Typverzweigungen zur Kompilierzeit, nimm if constexpr oder Concepts.

Das Tag-Dispatch-Muster

Tag Dispatch nutzt leere Struct-Typen ("Tags"), um zur Kompilierzeit zwischen Überladungen auszuwählen. Die Standardbibliothek setzt das ausgiebig ein — std::advance verzweigt über die Tags der Iterator-Kategorien. Vor if constexpr war dies die vorherrschende Technik für Verzweigungen zur Kompilierzeit.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: die moderne Alternative

Mit if constexpr bietet C++17 einen saubereren Weg, innerhalb einer einzigen Funktion zur Kompilierzeit zu verzweigen. Der verworfene Zweig wird nicht instanziiert und muss für den jeweiligen Typ daher gar nicht kompilierbar sein. Das ersetzt Tag Dispatch und SFINAE in einfachen Fällen weitgehend.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Best Practice

Nutze diesen Entscheidungsleitfaden für Verzweigungen zur Kompilierzeit:

- if constexpr (C++17) — am einfachsten, funktioniert innerhalb einer einzigen Funktion, ohne besondere Muster. Erste Wahl für unkomplizierte typbasierte Logik.
- Concepts/requires (C++20) — ideal, um Funktionsüberladungen oder ganze Templates einzuschränken. Selbsterklärend und mit klaren Fehlermeldungen.
- Tag Dispatch — weiterhin nützlich, wenn bereits eine Tag-Hierarchie existiert (etwa die Iterator-Kategorien). Codebasen vor C++17 setzen stark darauf.
- SFINAE / enable_if — die älteste Technik. Schwer zu lesen, kryptische Fehler. Vermeide sie in neuem Code, wenn Concepts oder if constexpr genügen.
- Vollständige Spezialisierung — für Klassen-Templates, wenn ein bestimmter Typ eine komplett andere Implementierung braucht.
- Niemals Funktions-Templates spezialisieren — überlade stattdessen.

Wichtigste Erkenntnisse
  • Die vollständige Spezialisierung (template <>) liefert eine maßgeschneiderte Implementierung für genau einen Typ
  • Partielle Spezialisierung schränkt einzelne Parameter ein — nur für Klassen-Templates verfügbar, nicht für Funktions-Templates
  • Die Spezialisierung von Funktions-Templates ist fehleranfällig — bevorzuge Überladung oder if constexpr
  • Tag Dispatch wählt über leere Struct-Typen zur Kompilierzeit die passende Überladung aus, typischerweise via iterator_traits
  • if constexpr (C++17) ist die moderne, sauberere Alternative und verwirft nicht genommene Zweige bereits zur Kompilierzeit

Quiz — Teste dein Wissen

(20 XP)

1. Warum solltest du es vermeiden, Funktions-Templates zu spezialisieren?

2. Welche Arten von Templates unterstützen partielle Spezialisierung?

3. Was macht `if constexpr` anders als ein gewöhnliches `if`?