Aller au contenu

constexpr, consteval et programmation à la compilation

Déplacez les calculs de l'exécution vers la compilation avec constexpr, consteval et constinit. Construisez des tables de correspondance, validez des configurations et utilisez des conteneurs constexpr.

La programmation à la compilation en C++

Le C++ vous permet, de façon unique, d'exécuter du vrai code à la compilation. Le compilateur peut ainsi calculer des valeurs, valider des entrées, construire des tables de correspondance et même exécuter des algorithmes complexes — le tout avant même la livraison de votre programme. Le résultat est intégré directement dans le binaire sous forme de constantes.

L'évolution de la programmation à la compilation en C++ :

- C++11 : introduction de constexpr — des fonctions simples, limitées à une seule instruction return
- C++14 : constexpr assoupli — boucles, variables locales, instructions multiples
- C++17 : if constexpr — branchement à la compilation qui écarte les chemins non pris
- C++20 : consteval (obligatoirement à la compilation), constinit (garantit que l'initialisation statique a lieu à la compilation), conteneurs constexpr (vector, string)
- C++23 : if consteval — détecte si vous êtes dans un contexte d'évaluation constante

Les fonctions constexpr

Une fonction constexpr peut être évaluée à la compilation ou à l'exécution. Si elle est appelée avec des arguments connus à la compilation dans un contexte qui exige une constante (argument de template, variable constexpr, static_assert, taille de tableau), le compilateur l'évalue à la compilation. Sinon, elle s'exécute à l'exécution comme une fonction 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 et constinit (C++20)

consteval marque une fonction qui doit être évaluée à la compilation — l'appeler avec des valeurs d'exécution est une erreur de compilation. On parle de « fonction immédiate ». constinit garantit qu'une variable à durée de stockage statique ou thread-local est initialisée à la compilation, ce qui évite le « fiasco de l'ordre d'initialisation statique » — mais contrairement à constexpr, la variable peut être modifiée après son initialisation.

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 en pratique

if constexpr (C++17) évalue une condition à la compilation et écarte entièrement la branche non prise. La branche écartée n'est pas instanciée — elle n'a pas besoin d'être du code valide pour les arguments de template donnés. C'est fondamentalement différent d'un if classique, où les deux branches doivent être valides.

Règles essentielles :

1. La condition doit être une expression constante évaluable à la compilation.
2. Dans un template, la branche écartée n'est pas instanciée pour les arguments de template courants.
3. En dehors d'un template, les deux branches doivent tout de même être syntaxiquement valides (elles ne seront simplement pas exécutées).
4. if constexpr peut apparaître dans des fonctions ordinaires, pas seulement dans des templates — mais l'élimination de branche ne s'applique que lors de l'instanciation d'un template.
5. Chaque branche d'un if constexpr a sa propre portée — les variables déclarées dans une branche ne sont pas visibles dans l'autre.

Conteneurs constexpr (C++20)

C++20 permet d'utiliser std::vector et std::string dans des contextes constexpr. Vous pouvez construire, manipuler et interroger des conteneurs à la compilation — une extension considérable des possibilités de la programmation à la compilation. La contrainte : l'allocation transitoire — la mémoire allouée pendant l'évaluation à la compilation doit être libérée avant la fin de l'évaluation constexpr.

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);

La programmation à la compilation en pratique

Le calcul à la compilation n'est pas un simple tour de passe-passe. Parmi ses usages concrets : construire des tables de correspondance intégrées au binaire, valider des configurations à la compilation, calculer des hachages pour comparer des chaînes à la compilation et générer des chemins de code optimisés.

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!
Piège

Erreurs courantes en programmation à la compilation :

1. Supposer que constexpr signifie « uniquement à la compilation » — une fonction constexpr peut aussi s'exécuter à l'exécution. Seul consteval garantit une évaluation à la compilation.
2. Allocation transitoire — dans les contextes constexpr de C++20, toute la mémoire allouée pendant l'évaluation doit être libérée avant la fin de celle-ci. Vous ne pouvez pas stocker un constexpr std::vector comme variable globale (sa mémoire sur le tas persisterait).
3. Toutes les fonctions ne sont pas compatibles avec constexpr — les entrées/sorties, la création de threads, reinterpret_cast et l'assembleur en ligne ne peuvent pas apparaître dans des contextes constexpr.
4. Abuser de constexpr — tout calculer à la compilation allonge les temps de build. Réservez-le aux valeurs qui gagnent réellement à être des constantes de compilation.

Points clés
  • Les fonctions constexpr s'exécutent à la compilation OU à l'exécution selon le contexte
  • consteval (C++20) impose une évaluation uniquement à la compilation — des arguments d'exécution provoquent une erreur de compilation
  • constinit (C++20) garantit l'initialisation à la compilation des variables statiques/thread-local, tout en autorisant leur modification à l'exécution
  • if constexpr écarte la branche non prise, qui n'a pas besoin d'être valide pour les arguments de template donnés
  • C++20 autorise std::vector et std::string dans les contextes constexpr, avec allocation transitoire
  • Les usages concrets incluent la génération de tables de correspondance, la validation de configurations et le hachage à la compilation

Quiz — Testez vos connaissances

(15 XP)

1. Quelle est la différence entre `constexpr` et `consteval` ?

2. Peut-on avoir un `constexpr std::vector<int>` comme variable globale en C++20 ?

3. Que garantit `constinit` ?