【问题标题】:Why doesn't std::function participate in overload resolution?为什么 std::function 不参与重载决议?
【发布时间】:2020-04-07 00:36:21
【问题描述】:

我知道下面的代码不会编译。

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 不考虑重载解决方案,正如我在this other post 中看到的那样。

我不完全理解迫使这种解决方案的技术限制。

我在 cppreference 上阅读了有关 phases of translationtemplates 的信息,但我想不出任何我找不到反例的推理。向半门外汉解释(对 C++ 还是新手),是什么以及在哪个阶段的翻译导致上述编译失败?

【问题讨论】:

  • @Evg 但在 OP 示例中只有一个重载可以被接受。在您的示例中,两者都会匹配

标签: c++ templates translation std-function


【解决方案1】:

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

请看,std::function&lt;R(Args)&gt; 并不要求给定函数完全属于R(Args) 类型。特别是,它不需要给它一个函数指针。它可以采用任何可调用类型(成员函数指针,一些具有operator() 重载的对象),只要它是可调用的就好像它采用了Args 参数并返回一些可转换的到 R(或者如果Rvoid,它可以返回任何东西)。

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

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

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

【讨论】:

  • 如果我错了,请原谅我,但是为什么 C++ 没有能力区分可用的重载呢?我现在看到 std::function 接受任何兼容类型,不仅是完全匹配,而且这两个 baz() 重载中只有一个是可调用的,好像它采用了指定的参数。为什么无法消除歧义?
  • 因为所有 C++ 看到的都是我引用的签名。它不知道它应该匹配什么。本质上,任何时候你对任何不清楚正确答案是什么的东西使用重载集时明确地来自 C++ 代码(代码是模板声明),语言迫使你拼出你的意思。
  • @TuRtoise:function 类模板上的模板参数无关。重要的是您正在调用的 constructor 上的模板参数。这只是typename F:又名,任何类型。
  • @TuRtoise:我认为你误解了一些东西。它是“只是 F”,因为这就是模板的工作方式。从签名中,该函数构造函数采用任何类型,因此任何使用模板参数推导调用它的尝试都会从参数中推导出类型。任何对重载集应用推导的尝试都是编译错误。编译器不会尝试从集合中推断出所有可能的类型以查看哪些类型有效。
  • “任何对重载集应用推导的尝试都是编译错误。编译器不会尝试从集中推导所有可能的类型以查看哪些类型有效。”正是我所缺少的,谢谢:)
【解决方案2】:

仅当 (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)...); }; \
    } \
  }

【讨论】:

    【解决方案3】:

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

    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 是 Callable.. . 并返回类型 R" 我不确定,但我几乎希望这足以解决歧义
    • @foreknownas_463035818 但是要确定它需要先推导出一个类型,因为它是一个重载的名称,所以不能。
    【解决方案4】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-08
      • 2023-03-25
      • 2011-06-11
      • 1970-01-01
      相关资源
      最近更新 更多