Чем принципиально отличается consteval от constexpr при вызове функции?
constexpr разрешает вычислить функцию во время компиляции, если контекст и аргументы это позволяют, но допускает и обычный runtime-вызов. consteval объявляет функцию немедленной: каждый её вызов должен быть вычислен во время компиляции, иначе программа некорректна.
Иными словами, constexpr — возможность, а consteval — требование. Это позволяет использовать consteval для жёсткой проверки того, что значение сформировано на этапе компиляции.
В C++11 появился constexpr, чтобы описывать функции и выражения, пригодные для вычисления во время компиляции. Однако одна и та же constexpr-функция могла вызываться и с константными, и с runtime-аргументами.
В C++20 появился consteval, решающий другую проблему: иногда runtime-вызов недопустим по смыслу задачи. Разработчику требовался механизм, который не просто позволял бы компилятору оптимизировать вычисление, а гарантировал бы его выполнение на этапе компиляции.
Предположим, функция строит значение, которое должно быть известно до запуска программы: идентификатор типа, маску свойств или результат разбора строкового литерала. Если объявить такую функцию только как constexpr, её можно будет вызвать с обычным runtime-значением, и ошибка обнаружится лишь в логике программы или на более позднем этапе.
Неверное понимание различия приводит к ложному ощущению compile-time гарантии. Оптимизатор может вычислить обычный вызов заранее, но это не является контрактом языка: при изменении контекста, уровня оптимизации или аргументов вычисление может остаться runtime.
Вызов constexpr-функции обязан быть константным только тогда, когда этого требует контекст, например при инициализации constexpr-переменной или формировании аргумента шаблона. В остальных случаях функция может работать во время выполнения.
Вызов consteval-функции является немедленным. Аргументы и весь вызов должны удовлетворять требованиям константного выражения; передача обычного значения, известного только во время выполнения, делает программу некорректной на этапе компиляции.
consteval не означает, что функция будет выполнена один раз при компиляции независимо от использования. Гарантия относится к каждому допустимому вызову: если вызов существует, он должен быть успешно вычислен компилятором.
consteval не заменяет constexpr. Если функцию полезно применять и с compile-time-, и с runtime-аргументами, следует выбрать constexpr. Если runtime-вызов должен быть запрещён самим интерфейсом, подходит consteval.
Ограничение consteval — более строгие требования к вызывающему коду. Оно может распространяться через несколько уровней шаблонов и усложнять интеграцию с данными, поступающими во время выполнения. Компромисс заключается в усилении гарантий компиляции ценой меньшей гибкости.
В библиотеке формировался числовой идентификатор из строкового литерала для регистрации типов. Требовалось, чтобы идентификатор не вычислялся во время выполнения и мог использоваться в контекстах, где нужна константа.
Рассматривались два варианта. constexpr был гибче и позволял переиспользовать функцию для runtime-строк, но не запрещал случайный вызов с такими строками. Обычная функция была проще, однако не давала compile-time-гарантии и могла добавлять работу при запуске.
Выбрали consteval для интерфейса, принимающего только compile-time-данные. Ошибки передачи runtime-значений стали диагностироваться компилятором, а вызывающий код явно выражал требование к моменту вычисления. Для отдельного runtime-сценария оставили другую функцию с constexpr или обычной семантикой.
Можно ли вызвать consteval-функцию с переменной, объявленной как const?
Не всякая const-переменная является константным выражением. Вызов допустим, только если значение переменной доступно компилятору как constant expression, например переменная инициализирована подходящим постоянным выражением. Само наличие квалификатора const этого не гарантирует.
Достаточно ли заменить consteval на constexpr, если компилятор обычно всё равно вычисляет функцию заранее?
Нет. Оптимизация и языковая гарантия — разные вещи. constexpr разрешает runtime-вызов, а фактическое предварительное вычисление зависит от конкретного вызова и правил компилятора; consteval делает нарушение требования диагностируемым.
Что происходит с consteval в шаблонном коде, если аргумент становится известен только при инстанцировании?
Проверка выполняется для конкретной инстанциации. Если после подстановки аргументы образуют константное выражение, вызов допустим; если аргумент зависит от runtime-данных, инстанциация с таким вызовом будет некорректной. Поэтому шаблонный интерфейс должен явно сохранять compile-time-контекст или разделять compile-time- и runtime-варианты.