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:
#include <concepts>
// note: there already is a concept for integral types
// also, we can use an abbreviated function template
void f(std::integral auto x) requires (x > 2)
{
// ...
}
void foo() {
f(0);
}
This produces the error:
<source>:3:40: error: substitution into constraint expression
resulted in a non-constant expression
void f(std::integral auto x) requires (x > 2)
^~~~~
<source>:9:5: note: while checking constraint satisfaction
for template 'f<int>' required here
f(0);
^
The value of x
is not known at compile time, and function constraints can only verify compile-time properties. Even though we are calling f
with x = 0
, f
needs to work with all possible arguments, not just 0
.
If you want a type which can only hold values greater than two, you can do that:
template <std::integral T>
class greater_two_integer {
private:
T v;
public:
greater_two_integer(T x) : v{x} {
assert(x > 2);
}
operator T() const noexcept {
// OPTIONAL: aid compiler optimizations
#if __has_cpp_attribute(assume)
[[assume(v > 2)]];
#elif defined(__clang__)
__builtin_assume(v > 2);
#elif __cpp_lib_unreachable == 202202L
// from <utility>
if (v <= 2) std::unreachable();
#endif
return v;
}
};
template <typename T>
void f(greater_two_integer<T> x) { /* ... */ }
void g(greater_two_integer<int> x) { /* ... */ }
int main() {
f(greater_two_integer{10}); // OK
g(10); // OK
f(greater_two_integer{0}); // runtime check fails
g(0); // runtime check fails
}
See live example
[[assume(v > 2)]]
We use [[assume]]
(since C++23) to enable compiler optimizations based on the fact that greater_two_integer
always 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 if v <= 2
.
This is safe, because the constructor contains assert(v > 2)
, meaning:
v > 2
first, andv > 2
Technically, you can break this class invariant by std::memcpy
ing into the class, or by writing its value through reinterpret_cast<int*>
.
However, with both of these methods you're obviously shooting yourself in the foot, they don't happen by accident.