Vai al contenuto

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.

constexpr_functions.cpp
#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.

consteval_constinit.cpp
#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.

constexpr_containers.cpp
#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.

practical_constexpr.cpp
#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!
Insidia

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.

Concetti chiave
  • Le funzioni constexpr vengono 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 compilazione
  • constinit (C++20) garantisce l'inizializzazione a tempo di compilazione delle variabili statiche o thread-local, ma ne consente la modifica a runtime
  • if constexpr scarta il ramo non preso, che non deve essere valido per gli argomenti template dati
  • C++20 permette std::vector e std::string nei 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`?