Введение
Современный C++ позволяет переносить часть вычислений с этапа выполнения программы на этап компиляции. Это не просто оптимизация скорости — это способ обнаруживать ошибки раньше, генерировать таблицы и константы без накладных расходов в runtime, и писать код, который одновременно работает и как обычная функция, и как генератор compile-time значений. Ключевое слово для этого — constexpr, появившееся в C++11 и существенно расширенное в C++14/17/20.
Суть концепции
constexpr означает «может быть вычислено во время компиляции», но не обязывает к этому — если аргументы известны только в runtime, функция выполнится как обычная. Если же все входные данные известны на этапе компиляции (например, литералы или другие constexpr-значения), компилятор имеет право (а в некоторых контекстах — обязан) вычислить результат заранее и подставить готовое значение в код.
#include <array>
#include <iostream>
// constexpr-функция: может выполняться и в compile-time, и в runtime
constexpr long long factorial(int n)
{
long long result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
// constexpr-функция для генерации таблицы факториалов на этапе компиляции
constexpr std::array<long long, 10> makeFactorialTable()
{
std::array<long long, 10> table{};
for (int i = 0; i < 10; ++i) {
table[i] = factorial(i);
}
return table;
}
// Таблица вычисляется один раз во время компиляции,
// в готовом бинарном файле уже лежат числа, без кода для их вычисления.
constexpr auto factorialTable = makeFactorialTable();
// static_assert проверяет утверждение во время компиляции
static_assert(factorialTable[5] == 120, "Ошибка в таблице факториалов");
int main()
{
constexpr int compileTimeValue = factorial(6); // вычислено компилятором
std::cout << "6! = " << compileTimeValue << std::endl;
int n;
std::cin >> n;
long long runtimeValue = factorial(n); // вычислено во время выполнения
std::cout << n << "! = " << runtimeValue << std::endl;
std::cout << "Таблица: ";
for (auto v : factorialTable) {
std::cout << v << ' ';
}
std::cout << std::endl;
return 0;
}
Разбор примера
Функция factorial помечена как constexpr, но содержит обычный цикл — начиная с C++14 в constexpr-функциях разрешены циклы, локальные переменные и условные операторы, то есть почти весь обычный императивный код (запрещены выделение динамической памяти со «сторонним эффектом» вне compile-time контекста, goto, неинициализированные переменные и обращение к статическим/глобальным неконстантным данным).
makeFactorialTable() вызывается в контексте инициализации constexpr переменной factorialTable, поэтому компилятор обязан выполнить вычисление на этапе компиляции — в итоговом бинарном файле просто будет лежать готовый массив чисел, без единой инструкции вычисления факториала в runtime.
static_assert — это проверка условия во время компиляции: если выражение false, компиляция прерывается с ошибкой. Это мощный инструмент для самопроверки библиотечного кода.
Вызов factorial(n), где n вводится пользователем, выполнится как обычная функция в runtime — компилятор не может вычислить её заранее, так как значение неизвестно до запуска программы. Это и есть гибкость constexpr: одна и та же функция работает в обоих режимах.
Подводные камни и советы
constexprне гарантирует compile-time вычисление, она лишь разрешает его. Чтобы заставить компилятор вычислить значение заранее, используйте результат в контексте, требующем константу:static_assert, размер массива, инициализацияconstexpr-переменной.- C++20 добавил
consteval— функции, которые обязаны выполняться только во время компиляции (попытка вызвать их с runtime-аргументом — ошибка компиляции). Используйтеconsteval, когда вычисление в runtime в принципе не имеет смысла. - Не путайте
constexprиconst.const— переменная не изменяется после инициализации, но инициализироваться может в runtime.constexpr— значение или функция в принципе может (или должно дляconsteval) быть вычислено на этапе компиляции. - C++20 разрешил
constexprдляstd::vectorиstd::stringпри определённых условиях (нужна динамическая память, выделяемая и освобождаемая в пределах того же constexpr-вычисления), что сильно расширило возможности compile-time программирования без шаблонной магии в духе C++03. - Избыточное усложнение. Не стоит превращать всё подряд в constexpr-метапрограммирование «для скорости» — современные компиляторы и так хорошо оптимизируют простые вычисления через constant folding. Используйте
constexprтам, где явно нужна гарантия compile-time вычисления или статическая проверка инвариантов.
constexpr — один из самых практичных инструментов современного C++: он почти не усложняет код по сравнению с шаблонной метапрограммацией старого стиля, но даёт ощутимый выигрыш в безопасности и производительности.
================================================================================