Использование лямбды для constexpr/consteval параметров функции
В C++ параметры constexpr/consteval функции сами не являются constexpr/consteval. Однако если обернуть передачу этих параметров в лямбду, то всё работает:
consteval auto foo(auto x) {
return x;
}
consteval auto bar(auto x) {
static_assert(foo(x) == x);
}
consteval auto baz(auto x) {
static_assert(foo(x()) == x());
}
int main(){
bar(42); // error: non-constant condition for static assertion
baz([]{return 42;}); // OK
}
Хоть у лямбды оператор вызова и является constexpr, т.к. он является членом, то должен неявно принимать объект лямбды, который не является константным выражением.
За счёт чего тогда это работает или это нелегальное использование? Желательно с указанием на пункты стандарта.
Ответы (2 шт):
Короткий ответ:
- Потому что
consteval auto foo(auto x)
является эквивалентом
template<typename T>
consteval auto foo(T x)
, а не
template<auto x>
consteval auto foo()
. Потому что правило для интерпретации auto-аргументов (placeholder-type-specifier), не зависит от наличия спецификатора consteval перед функцией. См. 9.3.3.5.18
- пример:
consteval void func(int n) {
static_assert( n>=0 ); // expression did not evaluate to a constant
}
должен порождать ошибку, независимо от того вызываем ли мы функцию func вообще, и с какими аргументами. см. https://stackoverflow.com/questions/57226797/will-consteval-allow-using-static-assert-on-function-arguments Потому что immediate function, это тоже function и проверка, что они не-illformed (т.е. что их можно скомпилировать.) производится при обработке тела функции, а не в момент вызова.
Развернутый ответ:
Заметим, что constexpr функции порождают по сути две функции: одну которая будет работать в run-time с произвольными аргументами, и одну которая будет работать с конкретными значениям в compile-time (immediate function).
consteval - обязаны порождать только compile-time функцию (immediate function), но требования и порядок компиляции для них, по сравнению с constexpr, неизменен. Cм. 9.2.5. [dcl.constexpr]
Есть дополнительные требования по использованию consteval, которые гарантируют, что компилятор всегда сможет вычислить результат функции как константное выражение.
Я не нашел, пункта, в котором указано что constexpr/consteval функции должны удовлетворять требованиям к обычным функциям. (Есть только ряд дополнительных требований, общих для consteval и constexpr: 9.2.5.3.) Видимо, это подразумевается, поскольку это тоже функции. Для constexpr это требование совершенно необходимо, поскольку они должны иметь возможность породить обычную (вызываемую в run-time) функцию.
Упростим ваш пример:
template<typename T>
consteval auto bar(T x) {
static_assert(x == x);
}
...
bar(42);
При компиляции для нас значимы три стадии:
- проверка шаблона функции (подстановка имен, независящих от типов аргументов)
- инстанционирование шаблона для конкретного типа (суть, компиляция). Именно на этом этапе будет проверен static_asset.
- подстановка конкретного константного значения и вычисление результата для него.
Если стадия 2 не будет выполнена, то до 3 дело не дойдет. Хотя продукт стадии 2 (скомпилированное тело функции), в случае consteval, нам и ненужен.
Хотя мы понимаем, что x==x всегда верно для всех встроенных типов (кроме double), тем не менее встроенный оператор == может вернуть конкретное значение на этапе компиляции, только если знает (на этапе компиляции) значения аргументов сравнения. Для произвольных значений аргументов, проверка того, что результат одинаков во всех случаях - разумным образом неразрешима (только перебрать все возможные комбинации аргументов). Поэтому, компилятор выдает ошибку на стадии 2. В тексте сообщения он будет ссылаться на строку bar(42);, поскольку конкретный тип аргумента станет известен только в этой строке, но это не стадия 3 (подстановка значения), как может показаться.
Заметим, что некоторые выражения, например sizeof(x), выдадут константное значение на шаге 2, поэтому их можно использовать в static_assert.
Почему работает с лямбдами?
В случае лямбды, вы фактически спрятали значение внутрь типа.
Поскольку в лямбде ничего не захватывается, и возвращаемое значение ни от чего не завист, то operator() является constexpr. Т.е. выражением x() так же не зависит только от конкретного значения переданного в функцию, также как и в случае sizeof(x).
Т.е. ваше решение - легальное, но неудобное.
Как исправить код, "чтобы работало"?
template<auto x>
consteval auto bar() {
static_assert(foo(x) == x);
}
...
bar<42>(); // ok
Здесь, значения x известны уже на стадии 2.
Или так:
consteval auto bar(auto x) {
return foo(x) == x;
}
...
static_assert( bar(42) ); // ok
Здесь static_assert-проверка будет выполнена уже после стадии 3.
static_assert объявление ожидает в скобках constant expression.
[dcl.pre]/nt:static_assert-declaration:
static_assert-declaration:
static_assert ( constant-expression ) ;
static_assert ( constant-expression , string-literal ) ;
Однако ни в одном из случаев static_assert(foo(x) == x), static_assert(x == x), static_assert(foo(x)), static_assert(x) для параметра x типа int выражения в скобках не будут constant expression, т.к. в них выполняется lvalue-to-rvalue преобразование для чтения значения из lvalue x.
An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine, would evaluate one of the following :
...
an lvalue-to-rvalue conversion
unless it is applied to
- a non-volatile glvalue that refers to an object that is usable in constant expressions, or
- a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
...
A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints:
- ...
В случае же лямбды с пустым списком захвата это преобразование не надо, т.к. она содержит оператор преобразования. И оператор преобразования, и оператор вызова при этом будут constexpr.
[expr.prim.lambda]/closure-11:
The conversion function or conversion function template is public, constexpr, non-virtual, non-explicit, const, and has a non-throwing exception specification. ...
The function call operator or any given operator template specialization is a constexpr function if either the corresponding lambda-expression's parameter-declaration-clause is followed by constexpr or consteval, or it satisfies the requirements for a constexpr function.
The definition of a constexpr function shall satisfy the following requirements:
- its return type (if any) shall be a literal type;
- each of its parameter types shall be a literal type;
- it shall not be a coroutine;
- if the function is a constructor or destructor, its class shall not have any virtual base classes;
- its function-body shall not enclose
- a goto statement,
- an identifier label,
- a definition of a variable of non-literal type or of static or thread storage duration.
Касательно вашего замечания:
т.к. он является членом, то должен неявно принимать объект лямбды, который не является константным выражением
Использование this в constexpr функции, являющейся частью рассматриваемого core constant expression, не делает это выражение не core constant expression.
An expression E is a core constant expression
unless the evaluation of E, following the rules of the abstract machine, would evaluate one of the following:
- this, except in a constexpr function that is being evaluated as part of E;
- ...