Зачем считать при компиляции
Всё, что можно вычислить до запуска программы, — это ноль циклов в рантайме, таблицы во флеше (важно для 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-кода.