constexpr в C++: простое практическое руководство (с consteval, constinit и if constexpr)
Запрос: constexpr в C++ — что это и как правильно использовать? В этой статье вы разберёте, как переносить вычисления на этап компиляции, ускорять код и повышать его надёжность с помощью constexpr. Мы также рассмотрим if constexpr, consteval и constinit (C++20), покажем практические примеры и разберём частые ошибки.
Что такое constexpr и чем он отличается от const
Ключевое слово constexpr говорит компилятору: «это можно вычислить во время компиляции». В итоге переменные, функции и даже конструкторы могут участвовать в константных выражениях. Важно понимать отличие от const:
- const — объект нельзя изменить после инициализации, но он не обязан быть известен на этапе компиляции.
- constexpr — значение должно быть вычислимо во время компиляции (если все аргументы тоже константны).
#include <array>
const int runtime_n = 10; // значение известно только в рантайме
constexpr int compiletime_n = 10; // известно на этапе компиляции
std::array<int, compiletime_n> ok{}; // работает
// std::array<int, runtime_n> bad{}; // ошибка: размер должен быть константой времени компиляции
constexpr-переменные и проверка через static_assert
Часто удобно сразу проверять инварианты:
#include <cstdint>
constexpr std::uint32_t kMagic = 0xDEADBEEF;
static_assert(kMagic == 0xDEADBEEF, "Неверная константа!");
Если условие в static_assert ложно, сборка упадёт ещё до запуска программы — отличный способ ловить ошибки раннее.
constexpr-функции: компилятор решит, когда вычислять
Функция, помеченная constexpr, может выполняться как на этапе компиляции, так и в рантайме — в зависимости от аргументов.
constexpr int sqr(int x) {
return x * x;
}
static_assert(sqr(3) == 9); // вычислится на этапе компиляции
int run(int n) {
return sqr(n); // вычислится в рантайме, если n не constexpr
}
Начиная с C++14, в constexpr-функциях разрешены локальные переменные, циклы и ветвления. Это делает их удобным инструментом для генерации таблиц, предрасчётов и валидации конфигурации.
Практика: предрасчёт таблицы квадратов
#include <array>
#include <cstddef>
constexpr std::array<int, 10> make_squares() {
std::array<int, 10> a{};
for (std::size_t i = 0; i < a.size(); ++i) {
a[i] = static_cast<int>(i * i);
}
return a;
}
constexpr auto kSquares = make_squares();
static_assert(kSquares[9] == 81);
Массив kSquares полностью рассчитан на этапе компиляции — нулевые накладные расходы в рантайме.
if constexpr: выбор ветки на этапе компиляции (C++17)
if constexpr удаляет неактуальную ветку кода ещё на этапе компиляции. Это особенно полезно в шаблонах:
#include <type_traits>
#include <limits>
#include <stdexcept>
template <class T>
T safe_div(T a, T b) {
if constexpr (std::is_integral_v<T>) {
if (b == 0) throw std::invalid_argument("division by zero");
return a / b; // целочисленное деление
} else {
return (b == T(0)) ? std::numeric_limits<T>::infinity() : a / b; // вещественное деление
}
}
Ветвь, не подходящая по условию, даже не компилируется, что позволяет использовать код, который для другого типа мог бы быть некорректным.
consteval: вычислить обязательно при компиляции (C++20)
consteval делает функцию немедленно вычисляемой: её вызовы допустимы только в контексте константных выражений.
consteval int inc(int x) { return x + 1; }
constexpr int a = inc(5); // ок
int foo(int v) {
// int r = inc(v); // ошибка: v не известно на этапе компиляции
return v + 1;
}
Используйте consteval для критичных констант, которые должны существовать только как compile-time значения (например, вычисление размера таблицы, проверка протокола, масок битов).
constinit: инициализация на этапе компиляции, но переменная изменяема (C++20)
constinit гарантирует, что объект со статическим временем хранения будет инициализирован на этапе компиляции (или, по крайней мере, до старта программы), но при этом он не константный.
// Файл конфигурации
constinit int g_port = 8080; // гарантированно инициализирован до main(), но менять можно
int main() {
g_port = 9090; // допустимо
}
constinit помогает избежать «order-of-initialization fiasco» между разными единицами трансляции и делает глобальные настройки предсказуемыми.
Пример: compile-time проверка параметров и конфигурации
#include <array>
#include <cstddef>
constexpr int kMaxUsers = 1024;
static_assert(kMaxUsers % 2 == 0, "kMaxUsers должен быть чётным для выравнивания");
constexpr int bucket_count(int users) {
return users / 2; // простое правило шардирования
}
constexpr auto kBuckets = bucket_count(kMaxUsers);
static_assert(kBuckets == 512);
struct Shard {
int id{};
};
constexpr std::array<Shard, kBuckets> make_shards() {
std::array<Shard, kBuckets> s{};
for (std::size_t i = 0; i < s.size(); ++i) s[i].id = static_cast<int>(i);
return s;
}
constexpr auto kShards = make_shards();
Если кто-то изменит kMaxUsers на некорректное значение, сборка упадёт. Это дисциплинирует конфигурацию и снимает класс ошибок из продакшена.
Ограничения и подводные камни
- Не всё можно в constexpr: ввод-вывод, системные вызовы, работа с внешними ресурсами — вне игры. Думайте о «чистых» функциях без побочных эффектов.
- Аргументы решают: constexpr-функция выполнится на этапе компиляции только при константных аргументах. Иначе — обычный рантайм.
- Не переусердствуйте: чрезмерные метапрограммы увеличивают время компиляции и сложность. Начинайте с простых и понятных оптимизаций.
- if constexpr отбрасывает «лишнюю» ветку, но только если условие можно вычислить на этапе компиляции.
- consteval строже, чем constexpr: вызов допустим только в константном контексте.
- constinit применим к объектам со статическим временем хранения; значение должно быть корректно инициализировано до запуска программы.
Микро‑советы и лучшие практики
- Используйте static_assert для проверки протоколов, размеров буферов и инвариантов на этапе компиляции.
- Выносите «дорогие» предсказуемые вычисления в constexpr-функции, генерируйте std::array с готовыми значениями.
- Для выборов по типам применяйте if constexpr и std::is_* типовые трейты.
- Оставляйте понятные имена константам и документируйте смысл — compile-time магия должна быть читаема.
- Собирайте с современным стандартом: -std=c++20 или выше, чтобы получить consteval/constinit и улучшения в constexpr.
Мини‑чеклист перед коммитом
- Можно ли посчитать это во время компиляции? Если да — вынесите в constexpr.
- Важно ли гарантировать compile-time вычисление? Если да — используйте consteval.
- Глобальная настройка должна быть инициализирована до main()? Подумайте о constinit.
- Покрыли ли ключевые инварианты static_assert-проверками?
Куда двигаться дальше
Освоив constexpr, вы начнёте писать более быстрый, предсказуемый и проверяемый на этапе компиляции код. Рекомендую прокачаться по современному C++ на практических задачах — это лучший способ закрепить материал. Посмотрите программу и первые уроки тут: Хочу прокачаться: курс «Программирование на C++ с Нуля до Гуру».
Итог: используйте constexpr для констант и функций, if constexpr для выбора ветки по типам, consteval когда нужно жёсткое compile-time, а constinit — для безопасной инициализации глобальных настроек. Это базовые кирпичики современного, аккуратного и быстрого C++‑кода.
-
Создано 29.05.2026 17:01:31
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.