Vilim Hrupelj
Vilim Hrupelj

Reputation: 11

Is there a way to evaluate a bundle of constexpr functions at compile time in C++?

I am new to C++ templates and I'm trying to make a constexpr template function that takes a parameter pack of other functions of return type bool (and in this example input type int) and checks if a given value satisfies all of them.

I've extracted the important bits of code and left some implementations that shouldn't be relevant out.

Item 1 below is how I'd like the function to behave where is_in_v is a metafunction that checks if a value of a given type is contained in the bundle of values after it. This works fine on its own, but complains when I try to pass func(b)... because b isn't a constant expression. This makes sense to me, however Item 2 seems to behave fine at compile time even though it is being passed the same value b. This makes me wonder if my idea is achieveable, because to me it feels like what works in Item 2 is in essence not that far from what I'm trying to do, but I am at a loss as to how to get around this error.

constexpr bool f1 (int a) { return true; }
constexpr bool f2 (int a) { return false; }
constexpr bool f3 (int a) { return a > 3; }

//Item 1
template<bool (*...func)(int)>
constexpr bool test(int b) {
    return !is_in_v<bool, false, func(b)...>;
}
//Item 2
template<bool (*func)(int)>
constexpr bool test2(int b) {
    return func(b);
}

...

int main () {
   static_assert(test<f1, f3>(5)); //ERROR: b is not a constant expression
   static_assert(test2<f3>(5)); //works fine
}

If anyone could tell me whether what I'm trying to do makes any sense or if there are alternative solutions I would greatly appreciate it.

I've tried passing by const reference and that worked for functions f1 and f2 but only delegated the problem of parameter b in test not being a constexpr to parameter a in f3.

Upvotes: 1

Views: 63

Answers (1)

!is_in_v<bool, false, func(b)...>;

This seems like a rather roundabout way of writing a variadic and-expression. C++17 supports fold expressions, so it really ought to be written more simply as

template<bool (*...func)(int)>
constexpr bool test(int b) {
    return (func(b) && ...);
}

The thing about templates, is that their non-type arguments must always and uncondionally be contant expressions. A regular function argument, even in a constexpr function, is never considered usable in a constant expression, and so can't be used to form one. It makes func(b) invalid as a template argument.

Roughly speaking test2 works because it doesn't assume b is usable in a constant expression. It simply lays out a computation, that when following the rules of the abstract machine with a compile time constant, the compiler can do. That's what constant evaluation is in a nutshell. When you mark a function as constexpr, then under certain conditions, the compiler may use it to evalute constants. But it doesn't mean anything about the function parameters being constant.

As a matter of fact, it can't mean that. Without delving into the details of the specificaiton, but to explain why that is, consider by contradiction if it did. Let's say you could write this:

constexpr auto foo(int a) {
    return std::integral_constant<int, a>{}; // Make believe it is valid to use a.
}

Then we get that foo(1) and foo(2) return different types! That's... not how c++ is designed to work. The function signature never depends on the values its parameters get! And I didn't even touch the fact constexpr functions exist at runtime since they are also regular functions, then the signature conundrum becomes even greater.

Upvotes: 1

Related Questions