Search code examples
c++constraintsc++20requires-clause

'requires' clause with fold expressions never satisfied


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 (well, for that part at least, since I have the same understanding struggles for almost anything that was added with ).


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 ?


Solution

  • 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

    Notes on [[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:

    • we always write a value v > 2 first, and
    • we can assume that we will later read a value v > 2

    Technically, you can break this class invariant by std::memcpying 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.