为什么 std::is_invocable 不能与自动推导出 return 类型的模板化 operator() 一起工作(例如,通用 lambda)

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

c++17 介绍 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.

但是,此模板不适用于 模板化运算符 (),其中(直接或间接)return 类型是自动推导的:

#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;
}

我猜可能和SFINAE有关。但这仍然不直观。我试图查看 N4659 draft 中介绍 std::is_invocable 的段落,但仍然找不到有关此行为的更多详细信息。由于本人不是这方面的专家,可能会有疏漏。

I guess the reason may be related to SFINAE.

确实如此。我们称之为 f1 "SFINAE-unfriendly":

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

那是因为 f1 宣传自己可以用 anything 调用(完全没有限制)但是要找出调用运算符实际上是什么 returns,你必须实例化调用操作符的主体。这涉及确定表达式 t.a() 的类型,但此时我们处于实例化的“直接上下文”之外。此时的任何失败都不是替换失败 - 这是一个硬编译错误。

f2另一方面:

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

是SFINAE-friendly。 t.a() 的检查发生在替换的直接上下文中,因此该表达式中的错误会导致该函数简单地从候选集中删除。 is_invocable 可以检查这个并确定 false.

f3f1 相同 - 虽然我们在直接上下文中检查 f1(t)),但 decltype(f1(t)) 的实际分辨率仍在直接上下文之外,因此它仍然是一个硬编译器错误。


简短的版本是:除非你是 SFINAE-friendly,否则任何类型特征或概念都不起作用。任何失败都必须在直接上下文中。