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


17

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

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 的翻译模板阶段,但是我无法想到找不到反例的任何理由。向半熟练的人解释(对于C ++来说仍然是新手),什么以及在什么翻译阶段使以上内容无法编译?


1
@Evg,但是只有一个重载在OPs示例中可以接受。在您的示例中,两者都会匹配
idclev 463035818 19/12/13

Answers:


13

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

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

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

该表达式baz表示一个过载集。如果使用该表达式调用重载集,那很好。如果使用该表达式作为采用特定函数指针的函数的参数,则C ++可以减少将重载设置为单个调用的方式,这样就可以使其正常工作。

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


如果我做错了什么,请原谅我,但是C ++为什么没有能力区分可用的重载?现在,我看到std :: function <T>可以接受任何兼容类型,不仅可以完全匹配,而且可以调用这两个baz()重载之一,就好像它采用了指定参数一样。为什么不可能消除歧义?
TuRtoise

因为所有C ++看到的都是我引用的签名。它不知道应该匹配什么。本质上,每当您对任何不明显来自C ++代码(该代码为模板声明)的正确答案使用过载集时,该语言都会迫使您拼写您的意思。
Nicol Bolas

1
@TuRtoise:function类模板上的template参数无关。重要的是要调用的构造函数上的template参数。这就是typename F:又名,任何类型。
Nicol Bolas

1
@TuRtoise:我认为您误会了一些东西。它就是“ F”,因为这就是模板的工作方式。从签名中,该函数构造函数可以采用任何类型,因此,使用模板参数推导进行调用的任何尝试都会从参数中推导该类型。将推论应用于重载集的任何尝试都是编译错误。编译器不会尝试从集合中推断出每种可能的类型,以查看哪些可行。
Nicol Bolas

1
“任何将推论应用于重载集合的尝试都是一个编译错误。编译器不会尝试从该集合中推断出每种可能的类型,以查看哪些可行。” 正是我所缺少的,谢谢:)
TuRtoise

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

作品。我们将重载分辨率推迟到存储在其中的lambda中fun,而不是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)...); };
  }
};

但这开始变得钝了。

现场例子

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

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

强制使用单一类型并允许扣减。


“因为baz是一个重载函数,所以无法推断出单一类型”,但是由于C ++ 14“除非f可调用参数类型Args ...并返回R类型,否则此构造函数不会参与重载解析”不确定,但是我几乎期望这足以解决歧义
idclev 463035818 19/12/13

3
@ formerlyknownas_463035818但是要确定这一点,它需要首先推断出一个类型,而不能,因为它是一个重载的名称。
NathanOliver

1

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

解决此问题的方法是使用显式告诉编译器您想要哪个重载static_cast

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.