Добавить в избранное

constexpr, consteval и if constexpr в C++: вычисления при компиляции

Чтение ≈ 4 мин

Как вычислять на этапе компиляции в C++: constexpr функции и переменные, consteval для гарантированного compile-time, if constexpr вместо SFINAE, static_assert и практическая польза для производительности.

Зачем считать при компиляции

Всё, что можно вычислить до запуска программы, — это ноль циклов в рантайме, таблицы во флеше (важно для embedded), гарантированная константность для case и размеров массивов, и ошибки, которые ловятся ещё до запуска. Механизм — constexpr и его младшие братья.

constexpr int maxClients = 10;              // константа времени компиляции
int buffer[maxClients];                     // размер массива — ок
static_assert(maxClients > 0, "must be >0"); // проверка при компиляции

constexpr переменные

constexpr у переменной означает «инициализируется при компиляции и неизменна»:

constexpr double pi = 3.14159265358979;
constexpr int square(int x) { return x * x; }

constexpr int area = square(5);   // вычислено компилятором — 25

В отличие от const, constexpr требует именно compile-time значение: const int x = rand(); — ок, constexpr int x = rand(); — ошибка.

constexpr функции

Функция, помеченная constexpr, может вычисляться как при компиляции, так и в рантайме — компилятор выбирает по контексту:

constexpr int fib(int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

constexpr int f10 = fib(10);   // точно при компиляции
int n = 15; fib(n);            // в рантайме — обычная функция

Ограничения постепенно снимались: C++14 разрешил циклы и локальные переменные внутри, C++20 — dynamic allocation в constexpr-контексте (для сборки строк и таблиц).

consteval и constinit (C++20)

consteval — функция, которая обязана вычислиться при компиляции:

consteval int mustBeCompileTime(int x) { return x * x; }

mustBeCompileTime(5);        // ок
// int y = mustBeCompileTime(rand());  // ошибка: рантайм-аргумент

constinit — гарантия статической инициализации (без dynamic init order fiasco):

constinit int globalTable[256];   // инициализируется до main, статически

if constexpr — условная компиляция внутри шаблона

if constexpr проверяет условие при компиляции и отбрасывает неподходящую ветку — компилятор не требует от неё валидности:

template<typename T>
auto process(T v) {
    if constexpr (std::is_integral_v<T>) {
        return v * 2;                 // только для целых
    } else if constexpr (std::is_floating_point_v<T>) {
        return v + 0.5;               // только для плавающих
    } else {
        return v;                     // остальное
    }
}

Это заменяет гору SFINAE/enable_if кода: ветка, которая не берётся, не инстанцируется — ошибки в ней не страшны.

static_assert — ошибки при компиляции

Проверка инвариантов до запуска:

template<typename T>
struct Buffer {
    static_assert(sizeof(T) <= 64, "T слишком большой для стека");
    static_assert(std::is_trivially_copyable_v<T>, "нужен POD");
    T data[16];
};

static_assert(sizeof(int) >= 4, "ожидали 32-битный int");

Практические сценарии

Таблицы значений

constexpr uint16_t sineTable[91] = { /* precomputed */ };
// во флеше, ноль операций в рантайме — важно для микроконтроллеров

Compile-time проверка параметров

constexpr int baudRate = 115200;
static_assert(baudRate >= 300 && baudRate <= 4000000, "недопустимый baud");

Вычислительные хэши/парсинг на этапе сборки

constexpr uint32_t hash(std::string_view s) {
    uint32_t h = 5381;
    for (char c : s) h = h * 33 + c;
    return h;
}

constexpr uint32_t id = hash("player_spawn");

Переход на compile-time для embedded

На AVR каждый байт флеша и каждая микросекунда цикла на счету. constexpr-таблицы синуса, константы UART, маски регистров — всё считается компилятором и лежит в программе как готовые данные.

Ограничения

  • constexpr-функция не может делать IO, бросать исключения (в constexpr-контексте), использовать глобальное состояние.
  • constexpr при не-constexpr аргументах — обычный вызов; для гарантии compile-time — consteval или сохранение в constexpr переменную.
  • Раздувает время компиляции — огромные таблицы могут замедлить сборку.

Итог

constexpr — «вычисляй как можно раньше». Минимум: constexpr-переменные вместо #define, static_assert для инвариантов, if constexpr вместо SFINAE, constexpr-функции для таблиц и констант. На embedded это буквально экономия флеша и тактов — на десктопе это гарантии корректности до запуска.

Полный пример: таблица при компиляции

constexpr int SQUARES[10] = [] {
    int t[10];
    for (int i = 0; i < 10; ++i) t[i] = i * i;
    return t;  // wait — массивы нельзя вернуть по значению, используем std::array
};

// правильно:
constexpr auto squares = [] {
    std::array<int, 10> t{};
    for (int i = 0; i < 10; ++i) t[i] = i * i;
    return t;
}();   // немедленный вызов — таблица готова при компиляции

static_assert(squares[5] == 25);

Вся математика сработала при компиляции — в рантайме только константный массив.

consteval для жёстких вычислений

consteval заставляет функцию вычисляться только при компиляции — вызов в рантайме не скомпилируется:

consteval int mustBeConst(int x) { return x * 2; }

constexpr int a = mustBeConst(5);  // ок
// int b = mustBeConst(getInt());  // ошибка компиляции

Удобно для констант, которые не должны «просочиться» в рантайм — например, строки-идентификаторы или захардкоженные ключи.

constinit: защита от SIOF

constinit гарантирует статическую инициализацию (до main) или выдаёт ошибку — защищает от static initialization order fiasco:

constinit int globalCounter = 42;   // гарантированно до main

Ключевые алгоритмы стали constexpr

С C++20 многие алгоритмы constexpr: std::sort, std::find, std::copy, std::fill, std::accumulate — можно сортировать и искать в компайл-тайме. Итог — целые препроцессорные структуры данных и константы без единого цикла в рантайме.

Мини-проект: CRC32 при компиляции

Напишите constexpr uint32_t crc32(const char* s) — побайтовый расчёт CRC таблицей, сгенерированной тоже в constexpr. Используйте static_assert(crc32("test") == 0x...). В рантайме строка превращается в уже вычисленную константу — классика compile-time C++.

Практический чек-лист

  • constexpr на переменную — значение при компиляции.
  • constexpr на функцию — может выполниться и в рантайме.
  • consteval — только при компиляции, ошибка при рантайм-вызове.
  • constinit — статическая инициализация до main, без SIOF.
  • if constexpr — compile-time ветвление в шаблонах.
  • static_assert — проверка при компиляции.
  • constexpr алгоритмы C++20 — sort/find/copy при компиляции.

Где это реально помогает

  • Таблицы CRC, синусов, коэффициентов — без runtime-вычислений.
  • Константные строки и хэши — constexpr хэш строки вместо макроса.
  • Валидация параметров при компиляции — static_assert на размеры.
  • Метапрограммирование — вычисления типов и констант без runtime-кода.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий