Заметки по языку с и с++

constexpr и compile-time вычисления в современном C++

1 просмотров

Введение

Современный 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++: он почти не усложняет код по сравнению с шаблонной метапрограммацией старого стиля, но даёт ощутимый выигрыш в безопасности и производительности.

================================================================================