EMZETT.
Login

Kurz: Templates sind Vorlagen für Funktionen und Klassen, die für beliebige Typen funktionieren. Der Compiler erzeugt aus ihnen beim Übersetzen den passenden Code, ohne Laufzeit-Overhead.

Teil des Kurses C++

Kapitel 9 von 15 im Kurs C++ (Abschnitt „Fortgeschritten“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Templates sind Vorlagen für Funktionen und Klassen, die für beliebige Typen funktionieren. Der Compiler erzeugt aus ihnen beim Übersetzen den passenden Code, ohne Laufzeit-Overhead. Die Standardbibliothek (vector<T>, map<K,V>, sort) besteht aus Templates.

Funktions-Templates

#include <iostream>
#include <string>
 
template <typename T>
T groesser(T a, T b) { return a > b ? a : b; }
 
template <typename T, typename U>
auto addiere(T a, U b) { return a + b; }
 
int main() {
    std::cout << groesser(3, 9) << ' ' << groesser(2.5, 1.5) << ' ' << groesser(std::string("a"), std::string("b")) << '\n';
    std::cout << addiere(1, 2.5) << ' ' << addiere(std::string("Ha"), "llo") << '\n';
    std::cout << groesser<double>(3, 2.5) << '\n';              // Typ explizit angeben
    return 0;
}

Ausgabe:

9 2.5 b
3.5 Hallo
3

Der Compiler erkennt T aus den Argumenten. Bei groesser(3, 2.5) würde das scheitern (zwei verschiedene Typen), dafür gibt es groesser<double>.

Klassen-Templates

#include <iostream>
#include <vector>
#include <stdexcept>
 
template <typename T>
class Stapel {
public:
    void push(const T& x) { daten_.push_back(x); }
    T pop() {
        if (daten_.empty()) throw std::out_of_range("Stapel leer");
        T x = daten_.back();
        daten_.pop_back();
        return x;
    }
    bool leer() const { return daten_.empty(); }
    std::size_t groesse() const { return daten_.size(); }
private:
    std::vector<T> daten_;
};
 
template <typename K, typename V>
struct Paar { K erstes; V zweites; };
 
int main() {
    Stapel<int> s;
    s.push(1); s.push(2); s.push(3);
    std::cout << s.pop() << s.pop() << ' ' << s.groesse() << '\n';
    Stapel<std::string> t;
    t.push("a");
    try { t.pop(); t.pop(); } catch (const std::out_of_range& e) { std::cout << e.what() << '\n'; }
    Paar<std::string, int> p{"Alter", 17};          // Typen werden erkannt (CTAD)
    std::cout << p.erstes << '=' << p.zweites << '\n';
    return 0;
}

Ausgabe:

32 1
Stapel leer
Alter=17

Concepts: Anforderungen an Typen (C++20)

Ohne Hilfe liefern falsche Template-Argumente riesige, schwer lesbare Fehlermeldungen. Concepts beschreiben, was ein Typ können muss:

#include <iostream>
#include <concepts>
#include <vector>
#include <string>
 
template <typename T>
concept Zahl = std::integral<T> || std::floating_point<T>;
 
template <Zahl T>
T quadrat(T x) { return x * x; }
 
template <typename T>
concept HatGroesse = requires(const T& t) { { t.size() } -> std::convertible_to<std::size_t>; };
 
template <HatGroesse C>
std::size_t laenge(const C& c) { return c.size(); }
 
auto verdopple(std::integral auto x) { return x * 2; }          // Kurzschreibweise
 
int main() {
    std::cout << quadrat(5) << ' ' << quadrat(1.5) << '\n';
    std::cout << laenge(std::vector<int>{1, 2, 3}) << ' ' << laenge(std::string("Hallo")) << '\n';
    std::cout << verdopple(21) << '\n';
    // quadrat("text");   // Fehler: "text" erfüllt Zahl nicht - mit klarer Meldung
    return 0;
}

Ausgabe:

25 2.25
3 5
42

Fertige Concepts gibt es in <concepts> (std::integral, std::same_as, std::totally_ordered, …) und für Bereiche in <ranges>.

Variadische Templates und Fold Expressions

#include <iostream>
 
template <typename... Args>
auto summe(Args... args) { return (args + ...); }                // Fold Expression (C++17)
 
template <typename... Args>
void ausgeben(const Args&... args) { ((std::cout << args << ' '), ...); std::cout << '\n'; }
 
int main() {
    std::cout << summe(1, 2, 3, 4) << ' ' << summe(1.5, 2.5) << '\n';
    ausgeben("a", 1, 2.5, 'c');
    return 0;
}

Ausgabe:

10 4
a 1 2.5 c

Template-Spezialisierung

#include <iostream>
#include <string>
 
template <typename T>
std::string beschreibe(const T&) { return "irgendwas"; }
 
template <>
std::string beschreibe<int>(const int& x) { return "int " + std::to_string(x); }       // Spezialfall
 
template <typename T>
std::string typ() {
    if constexpr (std::is_integral_v<T>) return "Ganzzahl";              // Auswahl zur Übersetzungszeit
    else if constexpr (std::is_floating_point_v<T>) return "Kommazahl";
    else return "anderes";
}
 
int main() {
    std::cout << beschreibe(5) << ", " << beschreibe(2.5) << '\n';
    std::cout << typ<int>() << ' ' << typ<double>() << ' ' << typ<std::string>() << '\n';
    return 0;
}

Ausgabe:

int 5, irgendwas
Ganzzahl Kommazahl anderes

if constexpr entscheidet beim Übersetzen, welcher Zweig existiert. Die nicht gewählten Zweige müssen nicht einmal übersetzbar sein.

Templates in Header-Dateien

Der Compiler braucht den vollständigen Code eines Templates an der Stelle, wo es benutzt wird. Deshalb stehen Templates (Deklaration und Umsetzung) in Header-Dateien (.h/.hpp), nicht in .cpp.

Merke

  • template <typename T> macht Funktionen und Klassen für viele Typen nutzbar, ohne Laufzeitkosten
  • Der Compiler erkennt Typen aus Argumenten; sonst f<int>(...) explizit
  • Concepts (requires, std::integral) formulieren Anforderungen und liefern klare Fehlermeldungen
  • Variadische Templates + Fold Expressions verarbeiten beliebig viele Argumente
  • if constexpr wählt Code zur Übersetzungszeit; Templates gehören in Header

Übungsaufgabe

Schreibe ein Template mittelwert für beliebige Container von Zahlen, eingeschränkt mit einem Concept.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Speicherverwaltung und Smart Pointer

Weiter: Fehlerbehandlung: Ausnahmen, optional und variant

Alle Kapitel: C++ im Überblick