I'm struggling a lot to understand how concepts and contraints works. Until now I always managed to avoid them with type traits and static_assert or std::enable_if (or even SFINAE) but I want to reconcile with c++20 (well, for that part at least, since I have the same understanding struggles for almost anything that was added with c++20).
I have a function with a variadic template parameter for which I want to accept only integral values that are above a threshold, let's say 2.
For that purpose I defined an integral concept, and then I add a requires clause to add the threshold constraint, which gives me this:
template <typename T>
concept integral = std::is_integral<T>::value;
template <integral ... Ts>
void f(Ts ... ts) requires (... && (ts > 2))
{
//blablabla
}
This compiles fine. But when I try to call f() with argumentsn for example f(8, 6);, I always get a compile-time error (GCC): error: 'ts#0' is not a constant expression
The full error trace (GCC):
<source>: In substitution of 'template<class ... Ts> requires (... && integral<Ts>) void f(Ts >...) requires (... && ts > 2) [with Ts = {int, int}]': <source>:15:6: required from here <source>:8:6: required by the constraints of 'template<class ... Ts> requires (... && integral<Ts>) void f(Ts ...) requires (... && ts > 2)' <source>:8:43: error: 'ts#0' is not a constant expression 8 | void f(Ts ... ts) requires (... && (ts > 2)) | ~~~~~~~~~~~~~~~^~ <source>: In function 'int main()': <source>:15:6: error: no matching function for call to 'f(int, int)' 15 | f(8, 6); | ~^~~~~~ <source>:8:6: note: candidate: 'template<class ... Ts> requires (... && integral<Ts>) void f(Ts >...) requires (... && f::ts > 2)' 8 | void f(Ts ... ts) requires (... && (ts > 2)) | ^ <source>:8:6: note: substitution of deduced template arguments resulted in errors seen above
What I don't understand is why are the arguments required to be constant expressions, and why is 8 not considered as such ?
The values of function parameters cannot be used in function constraints. A much simpler way to reproduce your problem is this:
This produces the error:
The value of
xis not known at compile time, and function constraints can only verify compile-time properties. Even though we are callingfwithx = 0,fneeds to work with all possible arguments, not just0.If you want a type which can only hold values greater than two, you can do that:
See live example
Notes on
[[assume(v > 2)]]We use
[[assume]](since C++23) to enable compiler optimizations based on the fact thatgreater_two_integeralways contains a value which is> 2. The attribute is applied to an empty statement in the conversion operator, so when we extract the value from the object, it is undefined behavior ifv <= 2.This is safe, because the constructor contains
assert(v > 2), meaning:v > 2first, andv > 2Technically, you can break this class invariant by
std::memcpying into the class, or by writing its value throughreinterpret_cast<int*>. However, with both of these methods you're obviously shooting yourself in the foot, they don't happen by accident.