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.
#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
#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.
#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)
}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.
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.
- Les templates variadiques utilisent
typename... Ts(pack de paramètres de template) etTs... 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::applydépaquette un tuple en arguments de fonction ;std::index_sequencepermet 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)` ?