传递重载函数指针及其参数时类型推导错误
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 每个目标函数名称)。
我试图提供一个围绕 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 每个目标函数名称)。