Search code examples
c++c++20compile-time

C++20 concept / requires expression to test if generic lambda accepts type


I'm trying to verify at compile time if a given lambda accepts a certain type (double in the case of my example code). As long as the signature of the lambda explicitly specifies the types this works. However, as soon as I use a generic lambda with auto in the signature, I get compilation errors when evaluating the requires statement.

The following code snippet illustrates the problem (also on compiler explorer)

#include <concepts>
#include <iostream>
#include <string>

template<typename F>
concept accepts_double = requires(F f, double d) { f(d); };

struct Component{};

int main(){
    auto f1 = [](double a){double b = a;};
    auto f2 = [](std::string a){std::string b = a;};
    auto f3 = [](auto a){std::string b = a;};

    std::cout << std::boolalpha << accepts_double<decltype(f1)> << "\n"; // expected true
    std::cout << std::boolalpha << accepts_double<decltype(f2)> << "\n"; // expected false

//This one gives the error:
    std::cout << std::boolalpha << accepts_double<decltype(f3)> << "\n"; // expected false, gives compilation error
}

I was under the impression that the requires statement would verify that f(d); was a valid expression and would return false if it was not. However, when compiling with the latest gcc and clang I get an error indicating that it is trying to evaluate the function body:

:13:38: error: no viable conversion from 'double' to 'std::string' (aka 'basic_string<char>')

My question Is there a different way to ensure that a lambda can be passed a double or are the requires statements restricted to checking the signature only?


Solution

  • This is expected behavior.

    The lambda f3 claims to accept anything. But to test if it can be invoked with a double, we need to instantiate the call operator. Only failures in the "immediate context" of that instantiation count as substitution failures -- where the immediate context is basically anything closely associated with the actual function signature. Once we get into the body of the call operator, that's not the immediate context anymore. We have to instantiate the body, that fails (can't construct a string from a double), but that's a hard compiler error.

    In other words, this is not SFINAE-friendly.


    Now, the only reason the body of the lambda needs to be instantiated is because it returns auto and we need to know the return type. We could instead provide that directly:

    auto f3 = [](auto a) -> void {std::string b = a;};
    

    Here, accepts_double<decltype(f3)> compiles. But it gives true! Because the lambda does claim to accept everything. There are no constraints here at all. We're only saved from compile failure by the fact that we happen to not have to instantiate the body.

    If we want to ensure that this both compiles and gives the correct answer, we need to add that constraint:

    auto f3 = [](std::convertible_to<std::string> auto a) {std::string b = a;};
    

    Now, we're actually constraining the argument to the allowed set of types. This both compiles and correctly yields false (since, of course, double is not convertible to std::string).