Template variadici ed espressioni fold
Gestisci un numero qualsiasi di argomenti template con i pack di parametri. Padroneggia le espressioni fold per elaborare i pack in modo elegante e senza ricorsione.
Pack di parametri
Un template variadico accetta un numero arbitrario di argomenti template usando un pack di parametri. La sintassi usa i puntini di sospensione (...). Esistono due tipi di pack:
1. Pack di parametri template — template — cattura zero o più argomenti di tipo.
2. Pack di parametri di funzione — void f(Ts... args) — cattura i corrispondenti argomenti della funzione.
Non puoi iterare direttamente su un pack con un ciclo. Devi invece espanderlo con l'operatore .... L'idea chiave è che l'espansione del pack applica un pattern a ogni elemento del pack e produce una lista separata da virgole.
sizeof...(Ts) e sizeof...(args) restituiscono il numero di elementi del pack come constexpr std::size_t.
Espansione ricorsiva del pack (pattern storico)
Prima delle espressioni fold di C++17, il modo standard di elaborare i pack era l'istanziazione ricorsiva: si stacca il primo elemento, lo si elabora e si ricorre sul resto. Serve un caso base per terminare la ricorsione.
#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
}Espressioni fold (C++17)
Le espressioni fold di C++17 eliminano la necessità di template ricorsivi nella maggior parte dei casi. Un'espressione fold applica un operatore binario a tutti gli elementi di un pack di parametri. Esistono quattro forme:
- Fold unario destro: (pack op ...) si espande in e1 op (e2 op (... op eN))
- Fold unario sinistro: (... op pack) si espande in ((e1 op e2) op ...) op eN
- Fold binario destro: (pack op ... op init) — include un valore iniziale
- Fold binario sinistro: (init op ... op pack) — include un valore iniziale
#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';
}Contesti di espansione dei pack
L'espansione di un pack (...) può comparire in molti contesti, non solo negli argomenti di una chiamata di funzione:
- Argomenti di funzione: f(args...) oppure f(transform(args)...)
- Argomenti template: std::tuple
- Classi base: class Derived : public Bases... { };
- Liste di inizializzazione: auto list = { args... };
- Catture delle lambda: [args...] { } oppure [...args = std::move(args)] { } (init-capture di C++20)
- Dichiarazioni using (C++17): using Bases::operator()...;
Il pattern è tutto ciò che precede i .... In f(transform(args)...) il pattern è transform(args), che si espande in f(transform(a1), transform(a2), transform(a3)).
È fondamentale capire che f(args)... non è la stessa cosa di f(args...). Il primo si espande in f(a1), f(a2), f(a3) (tre chiamate distinte in un'espressione con virgole), mentre il secondo si espande in f(a1, a2, a3) (una sola chiamata con tre argomenti).
std::tuple e std::apply
std::tuple è il contenitore eterogeneo della libreria standard, costruito sui template variadici. std::apply (C++17) invoca un callable spacchettando gli elementi della tupla come argomenti. std::index_sequence consente di indicizzare le tuple a tempo di compilazione.
#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)
}Gli errori più comuni con i template variadici sono:
1. Dimenticare i ... — scrivere args quando intendi args... lascia il pack non espanso e provoca un errore di compilazione.
2. Espandere nel punto sbagliato — f(g(args)...) e f(g(args...)) hanno significati molto diversi. Il primo chiama g una volta per elemento; il secondo chiama g con tutti gli elementi.
3. Pack vuoti — (... + args) con un pack vuoto è mal formato per la maggior parte degli operatori. Usa un fold binario con un valore iniziale: (0 + ... + args).
4. sizeof... restituisce il conteggio, non la dimensione in byte — sizeof...(args) dà il numero di elementi del pack, non la loro occupazione di memoria complessiva.
Preferisci le espressioni fold ai template variadici ricorsivi ogni volta che puoi. Sono più concise, più facili da leggere e spesso compilano più in fretta. Usa i fold binari con un valore iniziale esplicito per gestire i pack vuoti in sicurezza. Riserva l'espansione ricorsiva ai casi in cui le espressioni fold non bastano — per esempio quando devi elaborare gli elementi a coppie, oppure quando ogni elemento richiede una logica diversa.
- I template variadici usano
typename... Ts(pack di parametri template) eTs... args(pack di parametri di funzione) sizeof...(args)restituisce il numero di elementi di un pack a tempo di compilazione- L'espansione del pack applica un pattern a ogni elemento:
f(transform(args)...)chiama transform su ciascun elemento - Le espressioni fold (C++17) sostituiscono l'espansione ricorsiva:
(... + args)somma tutti gli elementi - Usa i fold binari con un valore iniziale per gestire in sicurezza i pack vuoti
std::applyspacchetta una tupla come argomenti di funzione;std::index_sequenceconsente l'indicizzazione a tempo di compilazione
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. In cosa si espande `(... + args)` con `args = {a, b, c}`?
2. Qual è la differenza tra `f(args...)` e `f(args)...`?
3. Cosa restituisce `sizeof...(args)`?