c++c++17language-lawyerreturn-type-deduction

Why doesn't std::is_invocable work with templated operator() which return type is auto-deduced (eg. generic lambdas)


c++17 introduces template <class Fn, class...ArgTypes> struct is_invocable:

Determines whether Fn can be invoked with the arguments ArgTypes.... Formally, determines whether INVOKE(declval<Fn>(), declval<ArgTypes>()...) is well formed when treated as an unevaluated operand, where INVOKE is the operation defined in Callable.

However, this template does not work with templated operator() which (direct or indirect) return type is auto-deduced:

#include <type_traits>
#include <iostream>

struct A {
  int a() { return 1; }
};

struct B {};

struct {
  template<typename T>
  auto operator()(T t) { return t.a(); }
} f1;

struct {
  template<typename T>
  auto operator()(T t) -> decltype(t.a()) { return t.a(); }
} f2;

struct {
  template<typename T>
  auto operator()(T t) -> decltype(f1(t)) { return f1(t); }
} f3;

template<typename F, typename T>
void check(F&& f, T) {
  std::cout << std::boolalpha << std::is_invocable_v<F, T> << std::endl;
}

int main() {
  check(f1, A());   // true
  check(f2, A());   // true
  check(f3, A());   // true
  //check(f1, B()); // error: ‘struct B’ has no member named ‘a’
  check(f2, B());   // false
  //check(f3, B()); // error: ‘struct B’ has no member named ‘a’
  return 0;
}

I guess the reason may be related to SFINAE. But this is still not intuitive. I tried to check N4659 draft's paragraphs which introduced std::is_invocable, but still couldn't find more detail about this behaivor. Since I am not an expert in this area, there may be omissions.


Solution

  • I guess the reason may be related to SFINAE.

    Indeed. We call things like f1 "SFINAE-unfriendly":

    struct {
      template<typename T>
      auto operator()(T t) { return t.a(); }
    } f1;
    

    That's because f1 advertises itself as being invocable with anything (there are no constraints at all) but to find out what the call operator actually returns, you have to instantiate the body of the call operator. That involves determining the type of the expression t.a(), but at this point we're outside of the "immediate context" of the instantiation. Any failure at this point is not a substitution failure - it's a hard compile error.

    f2 on the other hand:

    struct {
      template<typename T>
      auto operator()(T t) -> decltype(t.a()) { return t.a(); }
    } f2;
    

    is SFINAE-friendly. The check for t.a() happens in the immediate context of the substitution, so an error in that expression leads to the function simply being removed from the candidate set. is_invocable can check this and determine false.

    f3 is the same as f1 - while we check f1(t)) in the immediate context, the actual resolution of decltype(f1(t)) is still outside the immediate context, so it's still a hard compiler error.


    The short version is: no type traits or concepts work unless you're SFINAE-friendly. Any failures must be in the immediate context.