Le corutine di C++20
Le corutine, introdotte con C++20, rappresentano una delle aggiunte più ambiziose al linguaggio. Sono funzioni speciali capaci di sospendere la propria esecuzione e riprenderla in un momento successivo, mantenendo intatto il proprio stato interno tra una sospensione e l'altra. Questa capacità apre la porta a stili di programmazione eleganti per la generazione pigra di sequenze, la programmazione asincrona e la gestione di flussi di dati, che con le funzioni tradizionali richiederebbero macchine a stati complesse e artigianali.
Che cos'è una corutina
Una funzione ordinaria, una volta invocata, esegue il proprio corpo dall'inizio alla fine e restituisce il controllo al chiamante una sola volta, al termine. Una corutina, invece, può interrompersi in punti prestabiliti, restituire temporaneamente il controllo e poi riprendere esattamente da dove si era fermata, con tutte le variabili locali al loro posto. Una funzione diventa una corutina se il suo corpo contiene almeno una delle tre parole chiave dedicate: co_await, co_yield oppure co_return.
Le tre parole chiave
Ciascuna delle tre parole chiave esprime un tipo diverso di interazione. La parola chiave co_yield produce un valore e sospende la corutina, che potrà essere ripresa per produrre il valore successivo; è la base della generazione pigra di sequenze. La parola chiave co_await sospende la corutina in attesa che un'operazione, tipicamente asincrona, sia completata. La parola chiave co_return conclude la corutina restituendo un valore finale. Insieme, coprono i principali scenari d'uso.
Un generatore di valori
L'applicazione più immediata e intuitiva delle corutine è il generatore, una corutina che produce una sequenza di valori uno alla volta tramite co_yield. Ogni volta che il consumatore richiede il valore successivo, la corutina riprende, calcola il prossimo elemento, lo restituisce e si sospende nuovamente. In questo modo i valori vengono generati soltanto quando servono, senza mai costruire l'intera sequenza in memoria.
#include <generator>
#include <iostream>
// Genera i numeri da start in poi, uno alla volta
std::generator<int> countFrom(int start) {
int current = start;
while (true) {
co_yield current; // Produce un valore e si sospende
current++;
}
}
int main() {
int taken = 0;
// Consuma solo i primi cinque valori generati
for (int value : countFrom(1)) {
std::cout << value << std::endl;
if (++taken == 5) {
break;
}
}
return 0;
}
Il tipo std::generator, standardizzato con C++23, offre finalmente un supporto pronto all'uso per i generatori. Nonostante il ciclo all'interno della corutina sia infinito, il programma è perfettamente valido: la generazione si arresta non appena il consumatore smette di richiedere valori, grazie alla natura pigra delle corutine.
La macchina che sta dietro
Dietro l'apparente semplicità della sintassi, le corutine si appoggiano a un'infrastruttura sofisticata. Quando una corutina viene chiamata, il compilatore genera un oggetto che ne conserva lo stato tra le sospensioni, allocato in modo da sopravvivere oltre la singola chiamata. Il comportamento della corutina è governato da un tipo, detto tipo promessa, che definisce cosa accade nei momenti chiave del suo ciclo di vita: alla creazione, a ogni sospensione, alla produzione di un valore e alla conclusione. Le librerie, come quella che fornisce std::generator, incapsulano questa complessità dietro interfacce comode, così che l'utente comune non debba interagire direttamente con i meccanismi di basso livello.
La programmazione asincrona
Oltre alla generazione di sequenze, le corutine brillano nella programmazione asincrona. La parola chiave co_await permette di scrivere codice che attende il completamento di operazioni potenzialmente lente, come letture da rete o da disco, con una struttura lineare e leggibile, simile a quella del codice sincrono, ma senza bloccare il thread durante l'attesa. Questo elimina la frammentazione tipica del codice basato su callback annidate, rendendo i flussi asincroni più facili da scrivere e da comprendere. Il pieno sfruttamento di questa capacità richiede tipi di supporto che definiscano il significato dell'attesa, spesso forniti da librerie specializzate.
Uno strumento potente e in evoluzione
Le corutine sono uno strumento potente ma la cui infrastruttura di basso livello è complessa. Con C++20 il linguaggio ha introdotto i meccanismi fondamentali, e con C++23 sono arrivati i primi componenti di libreria pronti all'uso, come std::generator, che ne rendono l'adozione pratica per i casi comuni. Comprendere il concetto di sospensione e ripresa, e riconoscere i tre scenari coperti dalle parole chiave dedicate, permette di apprezzare come le corutine trasformino problemi tradizionalmente ostici, quali la generazione pigra e l'asincronia, in codice chiaro e naturale, destinato a diventare sempre più centrale nel C++ dei prossimi anni.