【问题标题】:Bad type deduction when passing overloaded function pointer and its arguments传递重载函数指针及其参数时的错误类型推导
【发布时间】:2020-06-20 10:13:40
【问题描述】:

我正在尝试为 std::invoke 提供一个包装器,以完成推断函数类型的工作,即使在函数重载时也是如此。
(我昨天向related question 询问了可变参数和方法指针版本)。

当函数有一个参数时,此代码 (C++17) 在正常重载条件下按预期工作:

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

对于更多的输入参数显然需要减少代码膨胀,但这是一个单独的问题。

如果用户的重载仅因 const/references 不同,如下所示:

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

然后Invoke推导函数不正确,有g++输出:

(int &)
(const int &)

(int &&)
(const int &)

还有clang++:

(int &)
(const int &)

(int &&)
(int &&)

(感谢 geza 指出 clang 的输出不同)。

所以Invoke 具有未定义的行为。

我怀疑元编程将是解决这个问题的方法。无论如何,是否可以在Invoke 站点正确处理类型推导?

【问题讨论】:

  • 预期输出是什么?是 (int&) (int&&) 吗?
  • @L.F.,是的。这些是 Foo 的输出,所以它们也应该是 Invoke 的输出。
  • 对我来说,clang 给出了不同的结果:对于第二种情况,它会打印两次 (int &amp;&amp;)
  • 肯定和S参数推导有关。尝试注释掉Invokeconst T &amp; 版本并注意错误。此外,如果显式提供了参数 (Invoke&lt;void&gt;(&amp;Foo, num)),则会调用正确的版本。
  • 这是第一种情况的理论:当编译器考虑非常量Invoke时,它可以同时使用常量和非常量Foo对其进行实例化。而且它不会检查两者的返回类型 (S) 是否相同,因此它说它不能推断出S。所以它忽略了这个模板。虽然实例化 const Invoke 只能使用 const Foo 来完成,所以在这种情况下它可以推导出 S。因此编译器使用这个模板。

标签: c++ templates function-pointers template-meta-programming type-deduction


【解决方案1】:

理论

对于each函数模板Invoke,模板参数推导(必须成功才能重载决议考虑)考虑eachFoo看是否可以然而,为所涉及的 one 函数参数 (func) 推导出许多模板参数(这里是两个)。只有当恰好有一个Foo 匹配时,整体推演才能成功(因为否则无法推演S)。 (这在 cmets 中或多或少都有说明。)

第一个(“按值”)Invoke 永远不会存在:它可以从任何Foos 中推导出来。同样,第二个(“非const 引用”)重载接受前两个Foos。请注意,这些不管其他参数适用于Invoke(对于arg)!

第三个(const T&amp;)重载选择对应的Foo重载,推导出T=int; last 对最后一个重载(其中 T&amp;&amp; 是一个正常的右值引用)做同样的事情,因此尽管它的 universal 引用(将 T 推断为 int&amp; (或const int&amp;) 在这种情况下与func 的推论相冲突。

编译器

如果arg 的参数是一个右值(并且像往常一样,不是 const),则两个似是而非的Invoke 重载都成功演绎,T&amp;&amp; 重载应该获胜(因为它绑定了一个右值引用到一个右值)。

对于来自 cmets 的案例:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

&amp;Bar 不进行任何推演,因为涉及到函数模板,因此在每种情况下都成功推演了 T(如 int)。然后,对每个案例再次进行推演,以识别要使用的Bar 特化(如果有),将U 推演为失败int&amp;const int&amp; , 和 int&amp; 分别。 int&amp; 的情况是相同的,而且明显更好,所以调用是模棱两可的。

所以 Clang 就在这里。 (但这里没有“未定义的行为”。)

解决方案

我没有给你一个通用的答案;由于某些参数类型可以接受多个值类别/常量限定对,因此在所有这些情况下正确模拟重载决策并不容易。有人提出以一种或另一种方式具体化重载集。您可能会考虑采用这些方面的当前技术之一(例如每个目标函数名称的通用 lambda)。

【讨论】:

    猜你喜欢
    • 2016-12-30
    • 2016-10-24
    • 2018-07-27
    • 2016-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-04
    • 1970-01-01
    相关资源
    最近更新 更多