Zum Inhalt springen

constexpr, consteval & Programmierung zur Kompilierzeit

Verlagere Berechnungen mit constexpr, consteval und constinit von der Laufzeit in die Kompilierzeit. Baue Lookup-Tabellen, prüfe Konfigurationen und nutze constexpr-Container.

Programmierung zur Kompilierzeit in C++

C++ erlaubt dir als eine der wenigen Sprachen, echten Code zur Kompilierzeit auszuführen. Der Compiler kann also Werte berechnen, Eingaben prüfen, Lookup-Tabellen aufbauen und sogar komplexe Algorithmen ausführen — alles, bevor dein Programm ausgeliefert wird. Das Ergebnis landet direkt als Konstante im Binary.

Die Entwicklung der Kompilierzeit-Programmierung in C++:

- C++11: constexpr wird eingeführt — einfache Funktionen mit einer einzigen return-Anweisung
- C++14: gelockertes constexpr — Schleifen, lokale Variablen, mehrere Anweisungen
- C++17: if constexpr — Verzweigung zur Kompilierzeit, die nicht genommene Pfade verwirft
- C++20: consteval (muss zur Kompilierzeit ausgewertet werden), constinit (stellt Initialisierung zur Kompilierzeit sicher), constexpr-Container (vector, string)
- C++23: if consteval — erkennt, ob du dich gerade in einer konstanten Auswertung befindest

constexpr-Funktionen

Eine constexpr-Funktion kann zur Kompilierzeit oder zur Laufzeit ausgewertet werden. Wird sie mit Argumenten aufgerufen, die zur Kompilierzeit feststehen, und zwar in einem Kontext, der eine Konstante verlangt (Template-Argument, constexpr-Variable, static_assert, Array-Größe), wertet der Compiler sie zur Kompilierzeit aus. Andernfalls läuft sie zur Laufzeit wie eine gewöhnliche Funktion.

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 & constinit (C++20)

consteval kennzeichnet eine Funktion, die zur Kompilierzeit ausgewertet werden muss — ein Aufruf mit Laufzeitwerten ist ein Kompilierfehler. Man spricht von einer "immediate function". constinit stellt sicher, dass eine Variable mit statischer oder Thread-lokaler Speicherdauer zur Kompilierzeit initialisiert wird, und verhindert so das "static initialization order fiasco" — anders als bei constexpr darf die Variable nach der Initialisierung aber verändert werden.

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 in der Praxis

if constexpr (C++17) wertet eine Bedingung zur Kompilierzeit aus und verwirft den nicht genommenen Zweig vollständig. Der verworfene Zweig wird nicht instanziiert — er muss für die gegebenen Template-Argumente also gar kein gültiger Code sein. Das unterscheidet ihn grundlegend von einem gewöhnlichen if, bei dem beide Zweige gültig sein müssen.

Die wichtigsten Regeln:

1. Die Bedingung muss ein konstanter Ausdruck zur Kompilierzeit sein.
2. In einem Template wird der verworfene Zweig für die aktuellen Template-Argumente nicht instanziiert.
3. Außerhalb eines Templates müssen beide Zweige weiterhin syntaktisch gültig sein (sie werden nur nicht ausgeführt).
4. if constexpr darf auch in gewöhnlichen Funktionen stehen, nicht nur in Templates — verworfen wird allerdings nur bei der Template-Instanziierung.
5. Jeder Zweig eines if constexpr hat seinen eigenen Gültigkeitsbereich — Variablen aus dem einen Zweig sind im anderen nicht sichtbar.

constexpr-Container (C++20)

C++20 erlaubt den Einsatz von std::vector und std::string in constexpr-Kontexten. Du kannst Container zur Kompilierzeit aufbauen, verändern und abfragen — eine enorme Erweiterung der Möglichkeiten. Die Einschränkung heißt transiente Allokation: Speicher, der während der Auswertung zur Kompilierzeit belegt wird, muss vor dem Ende dieser Auswertung wieder freigegeben sein.

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

Kompilierzeit-Programmierung in der Praxis

Berechnungen zur Kompilierzeit sind mehr als eine Spielerei. Typische Einsätze aus der Praxis: Lookup-Tabellen aufbauen, die fest im Binary landen, Konfigurationen schon beim Kompilieren prüfen, Hashes berechnen für den String-Abgleich zur Kompilierzeit und optimierte Codepfade erzeugen.

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!
Stolperfalle

Häufige Fehler bei der Kompilierzeit-Programmierung:

1. Annehmen, constexpr bedeute ausschließlich Kompilierzeit — eine constexpr-Funktion kann genauso zur Laufzeit laufen. Nur consteval garantiert die Auswertung zur Kompilierzeit.
2. Transiente Allokation — in constexpr-Kontexten ab C++20 muss aller während der Auswertung belegte Speicher vor deren Ende freigegeben sein. Ein constexpr std::vector lässt sich daher nicht als globale Variable ablegen (sein Heap-Speicher müsste fortbestehen).
3. Nicht jede Funktion ist constexpr-tauglich — Ein-/Ausgabe, Thread-Erzeugung, reinterpret_cast und Inline-Assembler haben in constexpr-Kontexten nichts verloren.
4. constexpr übertreiben — wer alles zur Kompilierzeit berechnet, verlängert die Build-Zeiten. Setze es dort ein, wo es sich wirklich lohnt, einen Wert als Kompilierzeit-Konstante zu haben.

Wichtigste Erkenntnisse
  • constexpr-Funktionen laufen je nach Kontext zur Kompilierzeit ODER zur Laufzeit
  • consteval (C++20) erzwingt die Auswertung zur Kompilierzeit — Laufzeitargumente sind Kompilierfehler
  • constinit (C++20) sichert die Initialisierung statischer bzw. Thread-lokaler Variablen zur Kompilierzeit, erlaubt aber Änderungen zur Laufzeit
  • if constexpr verwirft den nicht genommenen Zweig, der für die gegebenen Template-Argumente gar nicht gültig sein muss
  • C++20 erlaubt std::vector und std::string in constexpr-Kontexten, sofern die Allokation transient bleibt
  • Praktische Einsätze sind das Erzeugen von Lookup-Tabellen, das Prüfen von Konfigurationen und Hashing zur Kompilierzeit

Quiz — Teste dein Wissen

(15 XP)

1. Was ist der Unterschied zwischen `constexpr` und `consteval`?

2. Kannst du in C++20 einen `constexpr std::vector<int>` als globale Variable anlegen?

3. Was garantiert `constinit`?