Программирование во время компиляции с constexpr
Перенос вычислений из времени выполнения в компилятор - современный преемник метапрограммирования на шаблонах
constexpr сообщает компилятору, что функция или переменная пригодна для вычисления во время компиляции. Когда аргументы сами являются константами времени компиляции, результат вычисляет компилятор и запекает его в бинарный файл как литерал - процессору во время выполнения не остаётся никакой работы. Там, где метапрограммирование на шаблонах (TMP) кодировало вычисления в виде рекурсивных инстанцирований шаблонов, похожих на головоломки, constexpr позволяет писать обычные функции - с циклами, локальными переменными и if, - которые просто вдобавок выполняются во время компиляции.
- отличать функцию
constexpr(может выполниться при компиляции) от функцииconsteval(обязана); - понимать, когда переменная
constexprпринудительно вычисляется при компиляции и чем это отличается от обычногоconst; - использовать
if constexpr, чтобы отбрасывать ветви при компиляции и отказаться от значительной части кода на SFINAE и тег-диспетчеризации; - объяснять, почему
constexprв основном вытеснил TMP для вычисления значений, и называть случаи, где шаблоны и TMP по-прежнему нужны.
Тема опирается на Метапрограммирование на шаблонах, SFINAE и ограничения и Концепты (C++20) из этого курса. Предполагается, что вы уже читаете рекурсивный шаблон и трейт типа.
1. Функции constexpr - разрешение, а не обещание
Функция constexpr - это обычная функция с одним дополнительным свойством: ей разрешено выполняться во время компиляции. Произойдёт ли это на самом деле, целиком зависит от места вызова. Вызванная в контексте константного вычисления с константными аргументами, она вычисляется компилятором, и ответ сохраняется как литерал. Вызванная с обычным значением времени выполнения, она ведёт себя как любая другая функция и выполняется на процессоре. Одно определение - два возможных времени выполнения.
constexpr long factorial(int n) {
long result = 1; // C++14: локальные переменные, циклы
for (int i = 2; i <= n; ++i) // и мутация - всё разрешено
result *= i;
return result;
}
constexpr long f5 = factorial(5); // вычислит компилятор -> 120
int r = read_int(); // некое значение времени выполнения
long fr = factorial(r); // та же функция, выполняется во время работы
Обратите внимание, насколько это читаемее рекурсивного шаблонного факториала из темы Метапрограммирование на шаблонах. Это обычный императивный код. С C++14 функция constexpr может содержать циклы, несколько операторов, локальные переменные и их изменение; ограничение C++11 на единственный return давно снято.
Что считается контекстом константного вычисления? Места, где язык требует константное выражение:
- инициализатор переменной
constexprили условиеstatic_assert; - нетиповой аргумент шаблона, например
std::array<int, factorial(5)>; - размер массива, значение перечислителя, метка
case.
constexpr, которая ни при каких аргументах не может дать константу, некорректна (диагностика не обязательна). Но корректная функция constexpr, вызванная с аргументами времени выполнения, - это совершенно нормально: она просто выполняется во время работы.