Take a look at the following code
#include <type_traits>
template <typename T>
struct basic_type {
using type = T;
};
consteval auto foo(auto p, auto x) noexcept {
if constexpr (p(x)) {
return 1;
} else {
return 0;
}
}
int main() {
// This compiles
return foo(
[]<typename T>(basic_type<T>)
{
return std::is_integral_v<T>;
},
basic_type<int>{});
// This gives "x is not a constant expression"
/*return foo(
[]<typename T>(T)
{
return std::is_integral_v<std::decay_t<T>>;
},
0);*/
}
The first return statement compiles just fine on latest gcc trunk, while the second one does not compile, with the error message:
source>: In instantiation of 'consteval auto foo(auto:1, auto:2) [with auto:1 = main()::<lambda(T)>; auto:2 = int]':
<source>:26:12: required from here
<source>:9:3: error: 'x' is not a constant expression
9 | if constexpr (p(x)) {
| ^~
<source>: In function 'int main()':
<source>:26:19: error: 'consteval auto foo(auto:1, auto:2) [with auto:1 = main()::<lambda(T)>; auto:2 = int]' called in a constant expression
26 | return foo(
| ~~~^
27 | []<typename T>(T)
| ~~~~~~~~~~~~~~~~~
28 | {
| ~
29 | return std::is_integral_v<std::decay_t<T>>;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30 | },
| ~~
31 | 0);
| ~~
<source>:8:16: note: 'consteval auto foo(auto:1, auto:2) [with auto:1 = main()::<lambda(T)>; auto:2 = int]' is not usable as a 'constexpr' function because:
8 | consteval auto foo(auto p, auto x) noexcept {
| ^~~
Can anyone tell me why?
Here's a godbolt link https://godbolt.org/z/71rbWob4e
EDIT
As requested, here's foo without auto parameters:
template<typename Predicate, typename T>
consteval auto foo(Predicate p, T x) noexcept {
if constexpr (p(x)) {
return 1;
} else {
return 0;
}
}
Error Message looks like this:
<source>: In instantiation of 'consteval auto foo(Predicate, T) [with Predicate = main()::<lambda(T)>; T = int]':
<source>:27:15: required from here
<source>:10:3: error: 'x' is not a constant expression
10 | if constexpr (p(x)) {
| ^~
<source>: In function 'int main()':
<source>:27:15: error: 'consteval auto foo(Predicate, T) [with Predicate = main()::<lambda(T)>; T = int]' called in a constant expression
27 | return foo(
| ~~~^
28 | []<typename T>(T)
| ~~~~~~~~~~~~~~~~~
29 | {
| ~
30 | return std::is_integral_v<std::decay_t<T>>;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31 | },
| ~~
32 | 0);
| ~~
<source>:9:16: note: 'consteval auto foo(Predicate, T) [with Predicate = main()::<lambda(T)>; T = int]' is not usable as a 'constexpr' function because:
9 | consteval auto foo(Predicate p, T x) noexcept {
|
A constexpr function is a function that can be invoked within a constant expression. A constexpr function must satisfy the following conditions: It is not virtual. Its return type is a literal type.
The easiest way to check whether a function (e.g., foo ) is constexpr is to assign its return value to a constexpr as below: constexpr auto i = foo(); if the returned value is not constexpr compilation will fail.
constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time. A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations.
A call to a constexpr function produces the same result as a call to an equivalent non- constexpr function , except that a call to a constexpr function can appear in a constant expression. The main function cannot be declared with the constexpr specifier.
constexpr functions. A constexpr function is one whose return value can be computed at compile time when consuming code requires it. Consuming code requires the return value at compile time, for example, to initialize a constexpr variable or provide a non-type template argument.
In most cases you also cannot argue that you may prefer not to declare a function constexprsimply because you do not envisage any compile-time usage: because if others evtl. will use your code, they may see such a use that you do not. (But granted for type trait types and stuff alike, of course.)
When called with non- constexpr arguments, or when its value isn't required at compile time, it produces a value at run time like a regular function. (This dual behavior saves you from having to write constexpr and non- constexpr versions of the same function.) A constexpr function or constructor is implicitly inline.
When its arguments are constexpr values, a constexpr function produces a compile-time constant. When called with non-constexpr arguments, or when its value isn't required at compile-time, it produces a value at run time like a regular function.
In order to evaluate this:
if constexpr (p(x)) {
We need for p(x)
to be a constant expression. The rules for whether something qualifies as a constant expression or not are based on a list of things that you're not allowed to do.
When x
is a basic_type<int>
and p
is a function that takes a basic_type<int>
by value, there are simply no rules that we are violating. This is an empty type, so copying it (as we're doing here) doesn't actually involve any kind of read. This just works.
But when x
is an int
and p
is a function that takes an int
by value, this also requires copying x
but this time it involves reading the value of x
. Because, of course, gotta initialize the parameter somehow. And this does run afoul of a rule: [expr.const]/8 says we're not allowed to perform:
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;
An lvalue-to-rvalue conversion is what happens when we read the value of a variable, and neither of those bullets apply. It doesn't matter here that you don't actually care what the value is, since p
doesn't use it. In order to be able to even invoke p
, you have to copy x
, and you're not allowed to do that. Hence the error.
However, your lambda here doesn't actually need the value, just the type, so you could instead write this:
return foo(
[]<typename T>(T const&)
{
return std::is_integral_v<std::decay_t<T>>;
},
0);
Now we're no longer copying x
into the lambda, since the lambda no longer takes by value - it takes by reference. As a result, we're not violating the lvalue-to-rvalue conversion rule (or any other rule) and this is now a valid constant expression.
Then, as a bonus, if you change foo
to take x
by reference (because, again, you don't actually care about the value, so why not):
consteval auto foo(auto p, auto const& x) noexcept {
if constexpr (p(x)) {
return 1;
} else {
return 0;
}
}
Then both variants become ill-formed. Both the basic_type<int>
and int
versions (regardless of whether you take the int
by value or by reference). For more on this case, see the constexpr array size problem which I'm currently trying to resolve with P2280.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With