Aller au contenu

Templates variadiques et expressions de repli

Gérez un nombre quelconque d'arguments de template grâce aux packs de paramètres. Maîtrisez les expressions de repli (fold expressions) pour traiter les packs avec élégance, sans récursion.

Les packs de paramètres

Un template variadique accepte un nombre arbitraire d'arguments de template grâce à un pack de paramètres. La syntaxe utilise des points de suspension (...). Il existe deux sortes de packs :

1. Pack de paramètres de template — template — capture zéro ou plusieurs arguments de type.
2. Pack de paramètres de fonction — void f(Ts... args) — capture les arguments de fonction correspondants.

Vous ne pouvez pas parcourir directement un pack avec une boucle. À la place, vous le développez avec l'opérateur .... L'idée clé est que le développement de pack applique un motif à chaque élément du pack et produit une liste séparée par des virgules.

sizeof...(Ts) et sizeof...(args) renvoient le nombre d'éléments du pack sous forme de constexpr std::size_t.

Développement récursif de pack (motif historique)

Avant les expressions de repli de C++17, la manière standard de traiter les packs était l'instanciation récursive : on détache le premier élément, on le traite, puis on recommence récursivement sur le reste. Cela nécessite un cas de base pour terminer la récursion.

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
}

Expressions de repli (C++17)

Les expressions de repli (fold expressions) de C++17 éliminent dans la plupart des cas le besoin de templates récursifs. Une expression de repli applique un opérateur binaire à tous les éléments d'un pack de paramètres. Il en existe quatre formes :

- Repli unaire à droite : (pack op ...) se développe en e1 op (e2 op (... op eN))
- Repli unaire à gauche : (... op pack) se développe en ((e1 op e2) op ...) op eN
- Repli binaire à droite : (pack op ... op init) — inclut une valeur initiale
- Repli binaire à gauche : (init op ... op pack) — inclut une valeur initiale

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';
}

Contextes de développement de pack

Le développement de pack (...) peut apparaître dans de nombreux contextes, pas seulement dans les arguments d'appel de fonction :

- Arguments de fonction : f(args...) ou f(transform(args)...)
- Arguments de template : std::tuple
- Classes de base : class Derived : public Bases... { };
- Listes d'initialisation : auto list = { args... };
- Captures de lambda : [args...] { } ou [...args = std::move(args)] { } (init-capture de C++20)
- Déclarations using (C++17) : using Bases::operator()...;

Le motif est tout ce qui apparaît avant les .... Dans f(transform(args)...), le motif est transform(args), qui se développe en f(transform(a1), transform(a2), transform(a3)).

Il est crucial de bien le comprendre : f(args)... n'est pas équivalent à f(args...). Le premier se développe en f(a1), f(a2), f(a3) (trois appels distincts dans une expression virgule), tandis que le second se développe en f(a1, a2, a3) (un seul appel avec trois arguments).

std::tuple et std::apply

std::tuple est le conteneur hétérogène de la bibliothèque standard, construit sur les templates variadiques. std::apply (C++17) appelle un callable en lui passant les éléments du tuple dépaquetés comme arguments. std::index_sequence permet d'indexer les tuples à la compilation.

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)
}
Piège

Les erreurs les plus courantes avec les templates variadiques :

1. Oublier les ... — écrire args au lieu de args... laisse le pack non développé, ce qui provoque une erreur de compilation.
2. Développer au mauvais endroit — f(g(args)...) et f(g(args...)) ont des significations très différentes. Le premier appelle g une fois par élément ; le second appelle g avec tous les éléments.
3. Packs vides — (... + args) avec un pack vide est mal formé pour la plupart des opérateurs. Utilisez un repli binaire avec une valeur initiale : (0 + ... + args).
4. sizeof... renvoie le nombre d'éléments, pas la taille en octets — sizeof...(args) donne le nombre d'éléments du pack, pas leur taille mémoire cumulée.

Bonne pratique

Préférez les expressions de repli aux templates variadiques récursifs chaque fois que c'est possible. Elles sont plus concises, plus faciles à lire et compilent souvent plus vite. Utilisez des replis binaires avec une valeur initiale explicite pour gérer les packs vides en toute sécurité. Réservez le développement récursif aux cas où les expressions de repli ne suffisent pas — par exemple, lorsque vous devez traiter les éléments par paires, ou lorsque chaque élément nécessite une logique différente.

Points clés
  • Les templates variadiques utilisent typename... Ts (pack de paramètres de template) et Ts... args (pack de paramètres de fonction)
  • sizeof...(args) renvoie le nombre d'éléments d'un pack à la compilation
  • Le développement de pack applique un motif à chaque élément : f(transform(args)...) appelle transform sur chaque élément
  • Les expressions de repli (C++17) remplacent le développement récursif : (... + args) additionne tous les éléments
  • Utilisez des replis binaires avec une valeur initiale pour gérer les packs vides en toute sécurité
  • std::apply dépaquette un tuple en arguments de fonction ; std::index_sequence permet l'indexation à la compilation

Quiz — Testez vos connaissances

(20 XP)

1. En quoi se développe `(... + args)` pour `args = {a, b, c}` ?

2. Quelle est la différence entre `f(args...)` et `f(args)...` ?

3. Que renvoie `sizeof...(args)` ?