Zum Inhalt springen

Variadische Templates & Fold-Ausdrücke

Verarbeite beliebig viele Template-Argumente mit Parameter-Packs. Meistere Fold-Ausdrücke für elegante Pack-Verarbeitung ganz ohne Rekursion.

Parameter-Packs

Ein variadisches Template nimmt über ein Parameter-Pack beliebig viele Template-Argumente entgegen. Die Syntax nutzt drei Punkte (...). Es gibt zwei Arten von Packs:

1. Template-Parameter-Pack — template — erfasst null oder mehr Typargumente.
2. Funktions-Parameter-Pack — void f(Ts... args) — erfasst die zugehörigen Funktionsargumente.

Du kannst ein Pack nicht direkt in einer Schleife durchlaufen. Stattdessen expandierst du es mit dem ...-Operator. Entscheidend ist: Eine Pack-Expansion wendet ein Muster auf jedes Element des Packs an und erzeugt daraus eine kommaseparierte Liste.

sizeof...(Ts) und sizeof...(args) liefern die Anzahl der Elemente im Pack als constexpr std::size_t.

Rekursive Pack-Expansion (das alte Muster)

Vor den Fold-Ausdrücken aus C++17 war rekursive Instanziierung der übliche Weg, Packs zu verarbeiten: das erste Element abspalten, verarbeiten und mit dem Rest rekursiv weitermachen. Dafür braucht es einen Basisfall, der die Rekursion beendet.

recursive_variadic.cpp
#include <iostream>

// Base case: no arguments
void print() {
    std::cout << '\n';
}

// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) {
        std::cout << ", ";
    }
    print(rest...);  // expand rest, recurse
}

int main() {
    print(1, 2.5, "hello", 'A');
    // Output: 1, 2.5, hello, A

    print(); // just a newline
}

Fold-Ausdrücke (C++17)

Fold-Ausdrücke aus C++17 machen rekursive Templates in den meisten Fällen überflüssig. Ein Fold-Ausdruck wendet einen binären Operator auf alle Elemente eines Parameter-Packs an. Es gibt vier Formen:

- Unärer Rechts-Fold: (pack op ...) expandiert zu e1 op (e2 op (... op eN))
- Unärer Links-Fold: (... op pack) expandiert zu ((e1 op e2) op ...) op eN
- Binärer Rechts-Fold: (pack op ... op init) — mit einem Startwert
- Binärer Links-Fold: (init op ... op pack) — mit einem Startwert

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

// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
    return (... + args);  // ((a1 + a2) + a3) + ...
}

// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
    (std::cout << ... << args) << '\n';
    // expands to: ((std::cout << a1) << a2) << a3 ...
}

// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
    return (... && (args > 0));  // (a1>0) && (a2>0) && ...
}

// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
    (vec.push_back(std::forward<Ts>(args)), ...);
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
    print("x=", 10, " y=", 20);               // x=10 y=20
    std::cout << std::boolalpha
              << all_positive(1, 2, 3)   << '\n'  // true
              << all_positive(1, -2, 3)  << '\n'; // false

    std::vector<int> v;
    push_all(v, 10, 20, 30);
    for (int x : v) std::cout << x << ' ';  // 10 20 30
    std::cout << '\n';
}

Kontexte für die Pack-Expansion

Die Pack-Expansion (...) kann an vielen Stellen auftreten, nicht nur bei Funktionsargumenten:

- Funktionsargumente: f(args...) oder f(transform(args)...)
- Template-Argumente: std::tuple
- Basisklassen: class Derived : public Bases... { };
- Initialisierungslisten: auto list = { args... };
- Lambda-Captures: [args...] { } oder [...args = std::move(args)] { } (Init-Capture in C++20)
- using-Deklarationen (C++17): using Bases::operator()...;

Das Muster ist alles, was vor den ... steht. In f(transform(args)...) lautet das Muster transform(args), was zu f(transform(a1), transform(a2), transform(a3)) expandiert.

Das ist der springende Punkt: f(args)... ist nicht dasselbe wie f(args...). Ersteres expandiert zu f(a1), f(a2), f(a3) (drei einzelne Aufrufe in einem Komma-Ausdruck), Letzteres zu f(a1, a2, a3) (ein Aufruf mit drei Argumenten).

std::tuple & std::apply

std::tuple ist der heterogene Container der Standardbibliothek und baut auf variadischen Templates auf. std::apply (C++17) ruft ein Callable auf und entpackt dabei die Tupel-Elemente als Argumente. std::index_sequence ermöglicht die Indizierung von Tupeln zur Kompilierzeit.

tuple_apply.cpp
#include <tuple>
#include <iostream>
#include <string>
#include <utility>

// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
    std::cout << "Hello " << name << ", age " << age << '\n';
}

// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    std::cout << '(';
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});
    std::cout << ")\n";
}

int main() {
    auto person = std::make_tuple(std::string("Alice"), 30);
    std::apply(greet, person);  // Hello Alice, age 30

    auto data = std::make_tuple(1, 3.14, std::string("hello"));
    print_tuple(data);  // (1, 3.14, hello)
}
Stolperfalle

Die häufigsten Fehler bei variadischen Templates:

1. Die ... vergessen — schreibst du args, wo args... gemeint ist, bleibt das Pack unexpandiert und es gibt einen Kompilierfehler.
2. An der falschen Stelle expandiert — f(g(args)...) und f(g(args...)) bedeuten etwas völlig Verschiedenes. Das Erste ruft g einmal pro Element auf, das Zweite ruft g mit allen Elementen auf.
3. Leere Packs — (... + args) ist mit einem leeren Pack für die meisten Operatoren ungültig. Verwende einen binären Fold mit Startwert: (0 + ... + args).
4. sizeof... liefert die Anzahl, nicht die Größe in Bytes — sizeof...(args) gibt die Zahl der Pack-Elemente zurück, nicht ihren gesamten Speicherbedarf.

Best Practice

Bevorzuge Fold-Ausdrücke gegenüber rekursiven variadischen Templates, wann immer es geht. Sie sind knapper, leichter zu lesen und kompilieren oft schneller. Verwende binäre Folds mit explizitem Startwert, um leere Packs sicher zu behandeln. Rekursive Expansion bleibt den Fällen vorbehalten, in denen Fold-Ausdrücke nicht ausreichen — etwa wenn du Elemente paarweise verarbeiten musst oder wenn jedes Element eine andere Logik erfordert.

Wichtigste Erkenntnisse
  • Variadische Templates verwenden typename... Ts (Template-Parameter-Pack) und Ts... args (Funktions-Parameter-Pack)
  • sizeof...(args) liefert die Anzahl der Elemente eines Packs zur Kompilierzeit
  • Die Pack-Expansion wendet ein Muster auf jedes Element an: f(transform(args)...) ruft transform für jedes Element auf
  • Fold-Ausdrücke (C++17) ersetzen die rekursive Expansion: (... + args) summiert alle Elemente
  • Verwende binäre Folds mit Startwert, um leere Packs sicher zu behandeln
  • std::apply entpackt ein Tupel als Funktionsargumente; std::index_sequence ermöglicht die Indizierung zur Kompilierzeit

Quiz — Teste dein Wissen

(20 XP)

1. Wozu expandiert `(... + args)` für `args = {a, b, c}`?

2. Worin unterscheiden sich `f(args...)` und `f(args)...`?

3. Was liefert `sizeof...(args)`?