为什么 std::function 不参与重载决议?

Why doesn't std::function participate in overload resolution?

我知道以下代码无法编译。

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

因为据说 std::function 不考虑重载决议,正如我在 中读到的那样。

我不完全理解强制采用这种解决方案的技术限制。

我在 cppreference 上读到了 phases of translation and templates,但我想不出任何我找不到反例的推理。向半外行(对 C++ 还是新手)解释一下,是什么以及在哪个翻译阶段导致上述编译失败?

这与 "phases of translation" 没有任何关系。这纯粹是关于 std::function.

的构造函数

看,std::function<R(Args)> 不要求给定的函数完全 属于 R(Args) 类型。特别是,它不需要给它一个函数指针。它可以采用任何可调用类型(成员函数指针,某些具有 operator() 重载的对象)只要它是可调用的 就像 它采用 Args 参数一样并且 return 可以 转换为 R(或者如果 Rvoid,它可以 return 任何东西)。

为此,std::function 的适当构造函数必须是 模板 template<typename F> function(F f);。即可以取任意函数类型(受上述限制)。

表达式baz表示一个重载集。如果您使用该表达式来调用重载集,那很好。如果将该表达式用作采用特定函数指针的函数的参数,C++ 可以将重载集缩减为单个调用,从而使其正常。

但是,一旦函数是模板,并且您正在使用模板参数推导来确定该参数是什么,C++ 就不再能够确定重载集中的正确重载是什么。所以必须直接指定。

这里的问题是没有告诉编译器如何执行指针衰减函数。如果你有

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

然后代码就可以工作了,因为现在编译器知道你想要哪个函数,因为有一个你要分配给的具体类型。

当您使用 std::function 时,您调用它的函数对象构造函数,其形式为

template< class F >
function( F f );

并且由于是模板,所以需要推导传递的对象的类型。因为 baz 是一个重载函数,所以没有可以推导的单一类型,因此模板推导失败并且你得到一个错误。你必须使用

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

获取强制单类型并允许扣除。

仅当 (a) 调用 function/operator 的名称或 (b) 将其转换为具有显式签名的指针(指向函数或成员函数)时,才会发生重载解析。

这里都没有发生。

std::function 接受任何与其签名兼容 的对象。它不专门采用函数指针。 (lambda 不是 std 函数,std 函数也不是 lambda)

现在在我的自制函数变体中,对于签名 R(Args...) 我也接受 R(*)(Args...) 参数(完全匹配)正是出于这个原因。但这意味着它将 "exact match" 个签名提升到 "compatible" 个签名之上。

核心问题是重载集不是C++对象。您可以命名重载集,但不能将其传递给 "natively".

现在,您可以像这样创建函数的伪重载集:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

这将创建一个 C++ 对象,该对象可以对函数名称进行重载解析。

扩展宏,我们得到:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

写起来很烦人。一个更简单但用处稍差的版本在这里:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

我们有一个 lambda 接受任意数量的参数,然后将它们完美转发给 baz

然后:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

有效。我们将重载解析推迟到我们存储在 fun 中的 lambda 中,而不是直接传递 fun 一个重载集(它无法解析)。

至少有一项提议在 C++ 语言中定义一个将函数名转换为重载集对象的操作。在标准中出现这样的标准提案之前,OVERLOADS_OF 宏很有用。

您可以更进一步,支持转换为兼容函数指针。

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

但这开始变得迟钝了。

Live example.

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }

此时编译器正在决定将哪个重载传递给 std::function 构造函数,它所知道的是 std::function 构造函数被模板化为采用任何类型。它没有能力尝试两个重载并发现第一个不编译但第二个可以编译。

解决这个问题的方法是用 static_cast:

显式告诉编译器你想要哪个重载
Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}