constexpr, consteval e calcolo a compile time in C++
Una delle caratteristiche distintive del C++ è la capacità di spostare parte del lavoro dalla fase di esecuzione a quella di compilazione. Calcolare un valore durante la compilazione significa che il programma finito lo troverà già pronto, senza spendere tempo a determinarlo mentre gira. Questa filosofia, che unisce prestazioni e sicurezza, si è progressivamente ampliata negli standard moderni attraverso le parole chiave constexpr e consteval e il costrutto if constexpr.
Funzioni constexpr
Una funzione dichiarata constexpr può essere valutata in fase di compilazione, purché i suoi argomenti siano a loro volta costanti note al compilatore. Se invece viene chiamata con argomenti noti solo a runtime, la stessa funzione si comporta come una funzione ordinaria e viene eseguita durante l'esecuzione. Questa duplice natura la rende estremamente flessibile.
#include <iostream>
// Puo essere calcolata a compile time o a runtime
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
int main() {
// Calcolato durante la compilazione
constexpr int result = factorial(5);
std::cout << result << std::endl; // Il valore 120 e gia pronto
return 0;
}
Assegnando il risultato a una variabile a sua volta constexpr, si impone che il calcolo avvenga in fase di compilazione: se per qualche motivo non fosse possibile, il compilatore segnalerebbe un errore, garantendo che la valutazione sia davvero anticipata.
L'evoluzione di constexpr
Nelle prime versioni dello standard, le funzioni constexpr erano soggette a forti restrizioni, potendo contenere poco più di una singola espressione di ritorno. Gli standard successivi hanno progressivamente allentato questi vincoli, consentendo cicli, variabili locali, condizioni e strutture di controllo. Oggi è possibile scrivere algoritmi non banali interamente valutabili in fase di compilazione, avvicinando la programmazione compile-time alla normale programmazione.
// Un ciclo dentro una funzione constexpr e ora consentito
constexpr int sumUpTo(int n) {
int total = 0;
for (int i = 1; i <= n; i++) {
total += i;
}
return total;
}
consteval: le funzioni immediate
La parola chiave consteval, introdotta con C++20, definisce le cosiddette funzioni immediate, che devono obbligatoriamente essere valutate in fase di compilazione. A differenza di constexpr, non ammettono alcuna possibilità di esecuzione a runtime: ogni loro chiamata deve produrre un risultato costante, altrimenti il compilatore genera un errore. Sono adatte quando si vuole garantire in modo assoluto che un calcolo non gravi mai sull'esecuzione.
// Deve essere sempre valutata a compile time
consteval int square(int n) {
return n * n;
}
int main() {
constexpr int value = square(7); // Corretto: argomento costante
return 0;
}
if constexpr: la selezione a compile time
Il costrutto if constexpr, disponibile da C++17, consente di scegliere, in fase di compilazione, quale ramo di codice includere in base a una condizione costante. A differenza di un normale if, il ramo non selezionato viene completamente scartato e non deve nemmeno essere valido per i tipi in gioco. Questo lo rende prezioso all'interno dei template, dove il comportamento deve adattarsi al tipo con cui il template viene istanziato.
#include <type_traits>
#include <iostream>
template <typename T>
void describe(T value) {
// Il ramo scartato non viene nemmeno compilato
if constexpr (std::is_integral_v<T>) {
std::cout << "Tipo intero" << std::endl;
} else {
std::cout << "Tipo non intero" << std::endl;
}
}
La variabile std::is_integral_v proviene dall'intestazione type_traits, una raccolta di strumenti che permettono di interrogare le proprietà dei tipi in fase di compilazione, indispensabili nella metaprogrammazione.
Contenitori e algoritmi a compile time
Gli standard più recenti hanno esteso la valutazione costante a porzioni sempre più ampie della libreria standard. Molti algoritmi e persino alcuni contenitori possono ora essere utilizzati in contesti constexpr, consentendo di precalcolare tabelle, verificare invarianti e generare dati direttamente durante la compilazione. Questo riduce il carico a runtime e sposta il rilevamento di molti errori dal momento dell'esecuzione a quello della compilazione.
I vantaggi del calcolo anticipato
Spostare i calcoli alla compilazione offre due benefici principali. Il primo è prestazionale: ciò che è già calcolato non consuma tempo a runtime. Il secondo, altrettanto importante, è la sicurezza: un errore in un calcolo constexpr si manifesta durante la compilazione, cioè nel momento migliore per individuarlo e correggerlo. La programmazione a tempo di compilazione, un tempo territorio riservato a esperti di metaprogrammazione, è oggi accessibile e naturale grazie a constexpr, consteval e if constexpr, ed è uno degli aspetti in cui il C++ moderno esprime al meglio la propria combinazione di potenza ed efficienza.