Template variadici e parameter pack in C++
I template che abbiamo incontrato finora accettavano un numero fisso di parametri. Esistono però situazioni in cui è utile scrivere funzioni o classi capaci di ricevere un numero variabile di argomenti, potenzialmente di tipi diversi. I template variadici, introdotti con C++11, rispondono a questa esigenza attraverso il concetto di parameter pack, ovvero un insieme di parametri di lunghezza arbitraria. Sono lo strumento che sta alla base di molte funzioni della libreria standard, prime fra tutte quelle di formattazione e costruzione.
Il parameter pack
Un template variadico si dichiara indicando un parametro di tipo seguito da tre punti, che rappresenta un pacchetto di zero o più tipi. Analogamente, la funzione può ricevere un pacchetto di argomenti corrispondente. Il pacchetto non è una singola entità utilizzabile direttamente: deve essere espanso, cioè srotolato nei suoi elementi, con un'apposita sintassi.
#include <iostream>
// Accetta un numero qualsiasi di argomenti di tipi diversi
template <typename... Args>
void countArguments(Args... args) {
// sizeof... restituisce il numero di elementi nel pacchetto
std::cout << "Numero di argomenti: "
<< sizeof...(args) << std::endl;
}
int main() {
countArguments(1, "testo", 3.14, true); // Stampa 4
return 0;
}
L'operatore sizeof... restituisce, in fase di compilazione, il numero di elementi contenuti nel pacchetto. È il primo strumento per ragionare su un pacchetto senza ancora accedere ai singoli valori.
Espansione ricorsiva
Un approccio tradizionale per elaborare un pacchetto consiste nel trattarne un elemento alla volta, separando il primo argomento dal resto e richiamando ricorsivamente la funzione sul pacchetto rimanente. La ricorsione termina quando il pacchetto si esaurisce, grazie a una versione della funzione che gestisce il caso base.
#include <iostream>
// Caso base: nessun argomento residuo
void printAll() {
std::cout << std::endl;
}
// Elabora il primo argomento e ricorre sul resto
template <typename First, typename... Rest>
void printAll(First first, Rest... rest) {
std::cout << first << " ";
printAll(rest...); // Espande e passa il resto del pacchetto
}
int main() {
printAll(1, "due", 3.0);
return 0;
}
La sintassi con i tre punti dopo il nome del pacchetto, come in rest..., ne provoca l'espansione: il pacchetto viene srotolato nella lista di argomenti della chiamata successiva. Questo schema ricorsivo è stato per anni la tecnica standard di elaborazione dei pacchetti.
Le fold expression
A partire da C++17, le fold expression semplificano notevolmente l'elaborazione dei pacchetti, permettendo di applicare un operatore binario a tutti gli elementi senza scrivere alcuna ricorsione. Una fold expression racchiude tra parentesi il pacchetto e l'operatore, e il compilatore genera automaticamente la sequenza di operazioni. È il modo moderno e conciso di combinare tutti gli argomenti.
#include <iostream>
// Somma tutti gli argomenti con una fold expression
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // Applica l'operatore + a tutto il pacchetto
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // Stampa 15
return 0;
}
Le fold expression funzionano con la maggior parte degli operatori binari e possono includere un valore iniziale per gestire correttamente il caso di pacchetto vuoto. Rispetto alla ricorsione, riducono drasticamente il codice necessario e migliorano la leggibilità.
Applicare un'operazione a ogni elemento
Combinando le fold expression con l'operatore virgola è possibile eseguire un'azione su ciascun elemento del pacchetto, non solo combinarli in un unico risultato. Questo schema è utile, ad esempio, per stampare o elaborare ogni argomento separatamente in modo conciso.
#include <iostream>
template <typename... Args>
void printEach(Args... args) {
// Esegue l'espressione per ogni elemento del pacchetto
((std::cout << args << '\n'), ...);
}
L'importanza dei template variadici
I template variadici sono il fondamento di molte funzionalità essenziali del C++ moderno. La funzione std::make_unique, i costruttori che inoltrano un numero arbitrario di argomenti, le funzioni di formattazione e i contenitori che costruiscono elementi in loco si basano tutti su questa tecnica, spesso combinata con l'inoltro perfetto degli argomenti. Padroneggiare i parameter pack e le fold expression apre la strada alla scrittura di interfacce generiche e flessibili, capaci di adattarsi a un numero qualsiasi di parametri mantenendo al contempo la sicurezza dei tipi tipica del linguaggio.