为什么 std::is_invocable 不能与模板化的 operator() 一起工作,它的返回类型是自动推导的(例如通用 lambdas)

Vai*_*Man 1 c++ language-lawyer return-type-deduction c++17

C++17 介绍template <class Fn, class...ArgTypes> struct is_invocable

确定是否Fn可以使用参数调用ArgTypes...。正式地,确定INVOKE(declval<Fn>(), declval<ArgTypes>()...)当被视为未计算的操作数时是否格式良好,其中INVOKE定义的操作在Callable.

但是,此模板不适用于模板化的 operator(),其(直接或间接)返回类型是自动推导的:

#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;
}
Run Code Online (Sandbox Code Playgroud)

我想原因可能与SFINAE有关。但这仍然不直观。我试图检查引入 的 N4659 草案的段落std::is_invocable,但仍然无法找到有关此行为的更多详细信息。由于本人不是这方面的专家,可能有疏漏之处。

Bar*_*rry 5

我想原因可能与SFINAE有关。

的确。我们称之为f1“SFINAE 不友好”之类的东西:

struct {
  template<typename T>
  auto operator()(T t) { return t.a(); }
} f1;
Run Code Online (Sandbox Code Playgroud)

那是因为f1宣传自己可以用任何东西调用(根本没有任何约束),但要找出调用运算符实际返回的内容,您必须实例化调用运算符的主体。这涉及确定表达式的类型t.a(),但此时我们处于实例化的“直接上下文”之外。此时的任何失败都不是替换失败 - 这是一个硬编译错误。

f2 另一方面:

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

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

f3f1- 当我们f1(t))在直接上下文中检查时, 的实际解析decltype(f1(t))仍然在直接上下文之外,因此它仍然是一个硬编译器错误。


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