c++17 introduces template <class Fn, class...ArgTypes> struct is_invocable:
Determines whether
Fncan be invoked with the argumentsArgTypes.... Formally, determines whetherINVOKE(declval<Fn>(), declval<ArgTypes>()...)is well formed when treated as an unevaluated operand, whereINVOKEis the operation defined inCallable.
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.
Indeed. We call things like
f1"SFINAE-unfriendly":That's because
f1advertises 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 expressiont.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.f2on the other hand: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_invocablecan check this and determinefalse.f3is the same asf1- while we checkf1(t))in the immediate context, the actual resolution ofdecltype(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.