为什么std :: function不参与重载解析?

时间:2019-12-13 14:32:25

标签: c++ templates translation std-function

我知道以下代码不会编译。

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

正如我在this other post中所读,因为据说std::function不考虑过载解析。

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

我了解了有关cppreference的phases of translationtemplates的信息,但是我无法想到找不到反例的任何理由。向半熟练的人解释(对于C ++还是新手),什么以及在什么翻译阶段使以上内容无法编译?

4 个答案:

答案 0 :(得分:13)

这与“翻译阶段”实际上没有任何关系。纯粹是关于std::function的构造函数。

请参见,std::function<R(Args)>并不要求给定的函数完全是R(Args)类型的 。特别是,它不需要为其指定函数指针。它可以采用任何可调用的类型(成员函数指针,某些对象具有operator()的重载),只要它是可调用的就好像接受了Args参数并返回某些内容一样。 可转换为 R(或者如果Rvoid,则它可以返回任何内容)。

为此,std::function的适当构造函数必须是 template template<typename F> function(F f);。也就是说,它可以采用任何函数类型(受上述限制)。

表达式baz表示一个重载集。如果使用该表达式调用重载集,那很好。如果使用该表达式作为采用特定函数指针的函数的参数,则C ++可以减少对单个调用的重载设置,从而可以解决问题。

但是,一旦函数是模板,并且您正在使用模板参数推导确定该参数是什么,C ++将不再具有确定重载集中正确重载的能力。因此,您必须直接指定它。

答案 1 :(得分:7)

仅当(a)您正在调用函数/操作符的名称,或(b)将其强制转换为带有显式签名的指针(指向函数或成员函数)时,才会发生过载解析。

这里都没有发生。

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

现在在我的自制函数变体中,出于签名R(Args...)的原因,我也接受R(*)(Args...)参数(完全匹配)。但这意味着它将“完全匹配”签名提升到“兼容”签名之上。

核心问题是重载集不是C ++对象。您可以命名一个过载集,但不能“自然地”传递它。

现在,您可以创建一个如下所示的函数的伪重载集:

#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)...); }; \
    } \
  }

答案 2 :(得分:5)

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

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){}

只用一种类型就可以强制推演。

答案 3 :(得分:1)

在这一点上,编译器正在确定要传递给std::function构造函数的重载,它所知道的只是std::function构造函数的模板可采用任何类型。它没有能力尝试两个重载,并且发现第一个重载没有编译,但是第二个重载了。

解决此问题的方法是用static_cast明确告诉编译器您想要哪个重载:

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