传递重载函数指针及其参数时类型推导错误

Bad type deduction when passing overloaded function pointer and its arguments

我试图提供一个围绕 std::invoke 的包装器来完成推导函数类型的工作,即使在函数重载时也是如此。
(我昨天问了 变量和方法指针版本)。

当函数有一个参数时,此代码 (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 站点正确处理类型推导?

理论

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

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

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

编译器

如果 arg 的参数是一个右值(并且,像往常一样,不是 const),两个看似合理的 Invoke 重载都可以成功推导,并且 T&& 重载应该win(因为它将右值引用绑定到 rvalue)。

来自评论的案例:

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

由于涉及到函数模板,因此不会从 &Bar 进行推导,因此在每种情况下都成功推导了 T(如 int)。然后,对每种情况再次进行推导,以确定要使用的Bar专业化(如果有),将U推导为失败int&const int&int&int& 个案例是相同的,而且明显更好,所以这个调用是不明确的。

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

解决方案

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