constexpr, consteval e programmazione a tempo di compilazione
Sposta i calcoli dal runtime al tempo di compilazione usando constexpr, consteval e constinit. Costruisci tabelle di lookup, valida configurazioni e usa i contenitori constexpr.
La programmazione a tempo di compilazione in C++
Il C++ ti permette, cosa più unica che rara, di eseguire codice vero a tempo di compilazione. Il compilatore può calcolare valori, validare input, costruire tabelle di lookup e persino eseguire algoritmi complessi — tutto prima che il programma venga distribuito. Il risultato finisce direttamente nel binario sotto forma di costanti.
L'evoluzione della programmazione a tempo di compilazione in C++:
- C++11: introduzione di constexpr — funzioni semplici, con un solo return
- C++14: constexpr rilassato — cicli, variabili locali, più istruzioni
- C++17: if constexpr — ramificazione a tempo di compilazione che scarta i percorsi non presi
- C++20: consteval (obbligatoriamente a tempo di compilazione), constinit (garantisce che l'inizializzazione statica avvenga a tempo di compilazione), contenitori constexpr (vector, string)
- C++23: if consteval — rileva se ti trovi in un contesto di valutazione costante
Funzioni constexpr
Una funzione constexpr può essere valutata a tempo di compilazione oppure a runtime. Se viene chiamata con argomenti noti a tempo di compilazione in un contesto che richiede una costante (argomento template, variabile constexpr, static_assert, dimensione di un array), il compilatore la valuta a tempo di compilazione. Altrimenti viene eseguita a runtime come una funzione normale.
#include <iostream>
#include <array>
#include <cstdint>
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int next = a + b;
a = b;
b = next;
}
return b;
}
int main() {
// Compile-time evaluation: used in constexpr context
constexpr int fact10 = factorial(10); // computed at compile time
static_assert(fact10 == 3628800);
constexpr int fib10 = fibonacci(10); // computed at compile time
static_assert(fib10 == 55);
// Compile-time array sizes
std::array<int, factorial(5)> arr{}; // array of 120 elements
// Runtime evaluation: argument not constexpr
int n;
std::cin >> n;
std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
// Same function, now runs at runtime
}consteval e constinit (C++20)
consteval marca una funzione che deve essere valutata a tempo di compilazione — chiamarla con valori noti solo a runtime è un errore di compilazione. Si parla di "funzione immediata". constinit garantisce che una variabile con durata di memorizzazione statica o thread-local venga inizializzata a tempo di compilazione, prevenendo il famigerato "static initialization order fiasco" — ma, a differenza di constexpr, la variabile può essere modificata dopo l'inizializzazione.
#include <iostream>
// consteval: MUST be evaluated at compile time
consteval int square(int x) {
return x * x;
}
// constinit: must be initialized at compile time,
// but can be modified at runtime
constinit int global_value = square(7); // 49, computed at compile time
// constexpr variable: initialized at compile time AND immutable
constexpr int fixed_value = square(10); // 100, compile time, cannot change
int main() {
constexpr int a = square(5); // OK: compile-time context
static_assert(a == 25);
// int x = 5;
// int b = square(x); // ERROR: consteval requires compile-time args
// constinit variable can be modified after initialization
global_value = 100; // OK: runtime modification allowed
std::cout << global_value << '\n'; // 100
// fixed_value = 200; // ERROR: constexpr variable is const
}if constexpr nella pratica
if constexpr (C++17) valuta una condizione a tempo di compilazione e scarta completamente il ramo non preso. Il ramo scartato non viene istanziato: non deve nemmeno essere codice valido per gli argomenti template dati. È una differenza sostanziale rispetto a un if normale, dove entrambi i rami devono essere validi.
Regole fondamentali:
1. La condizione deve essere un'espressione costante valutabile a tempo di compilazione.
2. In un template, il ramo scartato non viene istanziato per gli argomenti template correnti.
3. Fuori da un template, entrambi i rami devono comunque essere sintatticamente validi (semplicemente non verranno eseguiti).
4. if constexpr può comparire anche in funzioni normali, non solo nei template — ma lo scarto si applica solo durante l'istanziazione di un template.
5. Ogni ramo di if constexpr ha il proprio scope — le variabili dichiarate in un ramo non sono visibili nell'altro.
Contenitori constexpr (C++20)
C++20 consente di usare std::vector e std::string in contesti constexpr. Puoi costruire, manipolare e interrogare contenitori a tempo di compilazione — un'enorme espansione delle possibilità della programmazione a tempo di compilazione. Il vincolo è l'allocazione transitoria: la memoria allocata durante la valutazione a tempo di compilazione deve essere liberata prima che la valutazione constexpr termini.
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <array>
// Build and process a vector at compile time (C++20)
constexpr int sum_of_squares(int n) {
std::vector<int> v;
for (int i = 1; i <= n; ++i) {
v.push_back(i * i);
}
return std::accumulate(v.begin(), v.end(), 0);
// vector memory freed here — transient allocation
}
// Compile-time string processing
constexpr std::size_t count_vowels(std::string_view sv) {
std::size_t count = 0;
for (char c : sv) {
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
++count;
}
}
return count;
}
// Generate a lookup table at compile time
template <std::size_t N>
constexpr auto generate_squares() {
std::array<int, N> result{};
for (std::size_t i = 0; i < N; ++i) {
result[i] = static_cast<int>(i * i);
}
return result;
}
static_assert(sum_of_squares(5) == 55); // 1+4+9+16+25
static_assert(count_vowels("Hello World") == 3);
constexpr auto squares = generate_squares<10>();
static_assert(squares[7] == 49);Programmazione a tempo di compilazione nella pratica
Il calcolo a tempo di compilazione non è solo un gioco di prestigio. Tra gli usi concreti ci sono: costruire tabelle di lookup incorporate nel binario, validare le configurazioni a tempo di compilazione, calcolare hash per il confronto di stringhe a tempo di compilazione e generare percorsi di codice ottimizzati.
#include <array>
#include <cstdint>
#include <stdexcept>
// Compile-time CRC32 table generation
constexpr auto generate_crc32_table() {
std::array<uint32_t, 256> table{};
for (uint32_t i = 0; i < 256; ++i) {
uint32_t crc = i;
for (int j = 0; j < 8; ++j) {
crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
}
table[i] = crc;
}
return table;
}
constexpr auto crc32_table = generate_crc32_table();
// Compile-time configuration validation
struct Config {
int port;
int max_connections;
int timeout_ms;
};
consteval Config validated_config(int port, int max_conn, int timeout) {
if (port < 1 || port > 65535)
throw "Port out of range"; // compile error if invalid!
if (max_conn < 1 || max_conn > 10000)
throw "Invalid max connections";
if (timeout < 0)
throw "Negative timeout";
return {port, max_conn, timeout};
}
// This validates at compile time — typo or bad value = build fails
constexpr auto config = validated_config(8080, 1000, 5000); // OK
// constexpr auto bad = validated_config(99999, 1000, 5000); // ERROR!Errori comuni nella programmazione a tempo di compilazione:
1. Dare per scontato che constexpr significhi solo tempo di compilazione — una funzione constexpr può essere eseguita anche a runtime. Solo consteval garantisce la valutazione a tempo di compilazione.
2. Allocazione transitoria — nei contesti constexpr di C++20, tutta la memoria allocata durante la valutazione deve essere liberata prima che la valutazione si concluda. Non puoi conservare un constexpr std::vector come variabile globale (la sua memoria sull'heap dovrebbe sopravvivere).
3. Non tutte le funzioni sono utilizzabili in constexpr — I/O, creazione di thread, reinterpret_cast e assembly inline non possono comparire in contesti constexpr.
4. Abusare di constexpr — calcolare tutto a tempo di compilazione allunga i tempi di build. Riservalo ai valori che traggono davvero vantaggio dall'essere costanti di compilazione.
- Le funzioni
constexprvengono eseguite a tempo di compilazione OPPURE a runtime, a seconda del contesto consteval(C++20) impone la valutazione esclusivamente a tempo di compilazione — gli argomenti runtime sono errori di compilazioneconstinit(C++20) garantisce l'inizializzazione a tempo di compilazione delle variabili statiche o thread-local, ma ne consente la modifica a runtimeif constexprscarta il ramo non preso, che non deve essere valido per gli argomenti template dati- C++20 permette
std::vectorestd::stringnei contesti constexpr, con allocazione transitoria - Tra gli usi pratici ci sono la generazione di tabelle di lookup, la validazione delle configurazioni e il calcolo di hash a tempo di compilazione
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Qual è la differenza tra `constexpr` e `consteval`?
2. In C++20 puoi avere un `constexpr std::vector<int>` come variabile globale?
3. Cosa garantisce `constinit`?