Перейти к содержимому

constexpr, consteval и Compile-Time программирование

Переместите вычисления из среды выполнения в компиляцию времени с помощью constexpr, consteval и constinit. Создавайте таблицы поиска, проверяйте конфигурации и используйте контейнеры Constexpr.

Программирование в C++

C++ позволяет запускать реальный код во время компиляции **. Это означает, что компилятор может вычислять значения, проверять вводимые данные, создавать таблицы поиска и даже запускать сложные алгоритмы — все до того, как ваша программа отправится. Результат встраивается непосредственно в двоичную как константы.

Эволюция компиляторного программирования в C++:

- C++11: constexpr - простые функции с однократным возвратом
C++14: Расслабленный constexpr — петли, локальные переменные, множественные утверждения
- C++17: if constexpr - ветвление времени компиляции, которое отбрасывает незанятые пути
- C++20: consteval (должен быть в режиме компиляции), constinit (обеспечивает статический init в режиме компиляции), контейнеры constexpr (vector, string)
- C++23: if consteval - определите, находитесь ли вы в контексте постоянной оценки

функции constexpr

Функция constexpr может быть оценена во время компиляции или . При вызове с аргументами времени компиляции в контексте, который требует постоянной (аргумент шаблона, переменная constexpr, static_assert, размер массива), компилятор оценивает его во время компиляции. В противном случае он работает во время выполнения, как обычная функция.

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 обозначает функцию, которая * должна быть оценена во время компиляции — называть ее со значениями времени выполнения является ошибкой компиляции. Это называется «непосредственная функция». constinit гарантирует, что переменная со статической или потоковой локальной продолжительностью хранения инициализируется во время компиляции**, предотвращая «фиксацию порядка статической инициализации» — но в отличие от constexpr, переменная может быть изменена после инициализации.

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 (C++17) оценивает состояние во время компиляции и полностью отбрасывает незанятую ветвь. Выброшенная ветвь не инстанцируется — она не должна быть действительным кодом для приведенных аргументов шаблона. Это принципиально отличается от обычного if, где обе ветви должны быть действительными.

Ключевые правила:

1.Условие должно быть постоянным выражением составляющего времени.
2. В шаблоне отбрасываемая ветвь не инстанцируется для текущих аргументов шаблона.
3.Вне шаблона обе ветви должны быть синтаксически действительными (они просто не будут выполняться).
4. if constexpr может появляться в обычных функциях, а не только в шаблонах, но и отбрасывание применяется только в создании шаблонов.
5.Каждая ветвь if constexpr имеет свою область действия — переменные, объявленные в одной ветви, не видны в другой.

Контейнеры Constexpr (C++20)

C++20 позволяет использовать std::vector и std::string в контекстах constexpr. Вы можете создавать, манипулировать и запрашивать контейнеры во время компиляции — огромное расширение возможностей программирования во время компиляции. Ограничение: временное выделение — память, выделенная во время оценки компиляции, должна быть освобождена до окончания оценки 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);

Практическое программирование компилятивного времени

Вычисление времени компиляции - это не просто трюк салона. Использование в реальном мире включает в себя: создание таблиц lookup, встроенных в двоичную систему, валидацию конфигураций во время компиляции, вычисление хешей для сопоставления строк в режиме компиляции и генерирование оптимизированных путей кода.

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!
Подводный камень

Типичные ошибки при программировании компиляции:

1. Предполагая, что constexpr означает только время компиляции — функция constexpr также может работать во время выполнения. Только consteval гарантирует оценку времени компиляции.
2. Временное распределение — в контекстах constexpr C++20 вся память, выделенная во время оценки, должна быть освобождена до завершения оценки. Вы не можете хранить constexpr std::vector как глобальный (его куча памяти будет сохраняться).
3. Не все функции являются constexpr-безопасными — I/O, создание потоков, reinterpret_cast и встроенная сборка не могут отображаться в контекстах constexpr.
4. Захватывающий constexpr — вычисление всего во время компиляции увеличивает время сборки. Зарезервируйте его для значений, которые действительно выигрывают от констант времени компиляции.

Ключевые выводы
  • Функции constexpr выполняются во время компиляции или во время выполнения в зависимости от контекста
  • consteval (C++20) сил компиляции-время-только оценка - аргументы во время выполнения компиляции ошибки
  • constinit (C++20) обеспечивает инициализацию по времени компиляции статических / локальных переменных, но позволяет изменять время выполнения
  • if constexpr отбрасывает незанятую ветвь, которая не должна быть действительной для приведенных аргументов шаблона.
  • C++20 позволяет использовать std::vector и std::string в констэксперских условиях с переходным распределением
  • Практическое использование включает генерацию таблиц поиска, проверку конфигурации и хеширование времени компиляции

Тест — проверьте свои знания

(15 XP)

1. В чем разница между `constexpr` и `consteval`?

2. Можно ли использовать `constexpr std::vector<int>` в качестве глобальной переменной в C++20?

3. Что гарантирует `constinit`?