Zum Inhalt springen

Funktions-Templates & Typdeduktion

Lerne, wie du Funktionen schreibst, die mit jedem beliebigen Typ funktionieren. Verstehe die Template-Argument-Deduktion, Nicht-Typ-Parameter und warum Templates in Header-Dateien stehen müssen.

Warum Funktions-Templates?

Ohne Templates bedeutet eine Funktion, die für mehrere Typen funktionieren soll, dass du dieselbe Logik dupliziert. Du würdest int max(int, int), double max(double, double) und std::string max(std::string, std::string) schreiben — identische Logik, für jeden Typ wiederholt. Mit Funktions-Templates schreibst du den Algorithmus einmal, und der Compiler erzeugt die typspezifischen Fassungen automatisch.

Ein Funktions-Template ist keine Funktion — es ist ein Bauplan, aus dem der Compiler echte Funktionen instanziiert, sobald du es mit konkreten Typen verwendest. Das geschieht vollständig zur Kompilierzeit und erzeugt Code, der identisch zu handgeschriebenem Code ist — ohne jeden Overhead.

Template-Syntax

Ein Funktions-Template wird mit dem Schlüsselwort template eingeleitet, gefolgt von einer Parameterliste in spitzen Klammern. Für Typparameter kannst du typename oder class beliebig austauschen — sie bedeuten exakt dasselbe. Die moderne Konvention bevorzugt typename.

function_template.cpp
#include <iostream>
#include <string>

// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7)          << '\n'; // deduces T = int
    std::cout << maximum(3.14, 2.71)    << '\n'; // deduces T = double
    std::cout << maximum<std::string>("hello", "world") << '\n';
    // explicit instantiation needed — "hello" is const char*, not std::string
}

Regeln der Template-Argument-Deduktion

Der Compiler leitet die Template-Argumente ab, indem er die Typen der Funktionsargumente gegen die Parametermuster abgleicht. Die wichtigsten Regeln sind:

1. Exakte Übereinstimmung bevorzugt — maximum(3, 7) deduziert T = int, weil beide Argumente int sind.
2. Keine impliziten Konvertierungen während der Deduktion — maximum(3, 7.0) ist ein Fehler: Der Compiler sieht T = int beim ersten und T = double beim zweiten Argument und weigert sich, eines davon auszuwählen.
3. Array-zu-Zeiger-Zerfall — wenn T als Wertparameter auftritt, zerfallen Arrays zu Zeigern. Verwende T(&)[N], um Array-Typ und -Größe zu erfassen.
4. Const/volatile auf oberster Ebene wird entfernt bei Wertparametern, bleibt aber bei Referenz- und Zeigerparametern erhalten.
5. Reference Collapsing — wird T als Referenztyp deduziert (bei Forwarding-Referenzen), greifen die Regeln des Reference Collapsing: T& & wird zu T&, T& && wird zu T&, T&& & wird zu T&, T&& && wird zu T&&.

Wenn die Deduktion fehlschlägt oder mehrdeutig ist, musst du die Template-Argumente explizit angeben: maximum(3, 7.0).

Nicht-Typ-Template-Parameter

Template-Parameter sind nicht auf Typen beschränkt. Nicht-Typ-Template-Parameter (NTTPs) nehmen Werte entgegen, die zur Kompilierzeit feststehen — etwa ganze Zahlen, Zeiger oder (seit C++20) Fließkomma- und Klassentypen. Sie sind ein mächtiges Mittel, um Kompilierzeit-Konstanten im Typsystem zu verankern.

non_type_params.cpp
#include <iostream>
#include <array>
#include <cstddef>

// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
    T total{};
    for (const auto& elem : arr) {
        total += elem;
    }
    return total;
}

// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;

int main() {
    std::array<int, 4> nums = {1, 2, 3, 4};
    std::cout << sum(nums) << '\n';     // 10, deduces T=int, N=4

    std::cout << doubled<21> << '\n';   // 42
    std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}
Hinweis

Jede eindeutige Kombination von Template-Argumenten veranlasst den Compiler, eine eigene Instanziierung zu erzeugen — eine separate Kopie der Funktion in deinem Binary. Aufrufe von maximum, maximum und maximum erzeugen drei Funktionen. Meist ist das unproblematisch, doch in großen Codebasen können Templates zu Code-Bloat beitragen (größere Binaries, längere Kompilierzeiten). Gegenmittel sind unter anderem: extern template-Deklarationen, um zu steuern, wo instanziiert wird, das Auslagern typunabhängigen Codes aus dem Template heraus und der Einsatz von Type Erasure, wenn Polymorphie zur Laufzeit akzeptabel ist.

Stolperfalle

Weil der Compiler die vollständige Template-Definition sehen muss, um sie zu instanziieren, können Templates nicht wie gewöhnliche Funktionen auf .h- und .cpp-Dateien aufgeteilt werden. Steckst du eine Template-Definition in eine .cpp-Datei und versuchst, sie aus einer anderen Übersetzungseinheit zu verwenden, bekommst du einen Linker-Fehler (undefined reference). Die übliche Lösung ist, die gesamte Template-Definition in die Header-Datei zu schreiben. Werden die Kompilierzeiten zum Problem, kannst du auf explizite Instanziierung zurückgreifen: Die Definition bleibt in der .cpp-Datei, und du fügst template int maximum(int, int); hinzu, um genau die Spezialisierungen zu instanziieren, die du brauchst.

Explizite Instanziierung

Du kannst explizit steuern, wo und wann Templates instanziiert werden. In großen Projekten senkt das die Kompilierzeiten, weil redundante Instanziierungen über mehrere Übersetzungseinheiten hinweg vermieden werden.

explicit_instantiation.h
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);

// ---- maximum.cpp ----
#include "maximum.h"

// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);
Wichtigste Erkenntnisse
  • Funktions-Templates sind Baupläne — der Compiler erzeugt konkrete Funktionen für jeden Satz von Template-Argumenten
  • typename und class sind in Template-Parameterlisten austauschbar
  • Die Template-Argument-Deduktion führt keine impliziten Konvertierungen durch — mehrdeutige Deduktionen sind Fehler
  • Nicht-Typ-Template-Parameter verankern Kompilierzeit-Werte im Typsystem
  • Templates müssen in Headern definiert werden (oder explizit instanziiert), weil der Compiler zum Instanziieren die vollständige Definition braucht

Quiz — Teste dein Wissen

(15 XP)

1. Was passiert beim Aufruf `maximum(3, 7.0)` mit dem Template `template<typename T> T maximum(T a, T b)`?

2. Warum müssen Template-Definitionen in der Regel in Header-Dateien stehen?

3. Wozu dient `extern template int maximum<int>(int, int);`?