鉴于以下代码,歧义背后的原因是什么?我可以规避它还是必须保留(讨厌的)明确的演员?
#include <functional>
using namespace std;
int a(const function<int ()>& f)
{
return f();
}
int a(const function<int (int)>& f)
{
return f(0);
}
int x() { return 22; }
int y(int) { return 44; }
int main()
{
a(x); // Call is ambiguous.
a(y); // Call is ambiguous.
a((function<int ()>)x); // Works.
a((function<int (int)>)y); // Works.
return 0;
}
有趣的是,如果我使用a()
参数注释function<int ()>
函数并在我的main中调用a(x)
,则编译正确失败,因为x
之间的类型不匹配以及唯一可用的function<int (int)>
函数的参数a()
。如果编译器在这种情况下失败,为什么在存在两个a()
函数时会出现歧义?
我尝试过使用VS2010和g ++ v.4.5。两者都给我完全相同的歧义。
答案 0 :(得分:40)
问题是function<int()>
和function<int(int)>
都可以从同一个函数构造出来。这就是std::function
的构造函数声明在VS2010中的样子:
template<class _Fx>
function(_Fx _Func, typename _Not_integral<!_Is_integral<_Fx>::value, int>::_Type = 0);
忽略SFINAE部分,几乎可以构建任何东西
std::/boost::function
使用一种称为类型擦除的技术,允许传入任意对象/函数,只要它们在被调用时满足签名即可。其中一个缺点是,当提供一个无法像签名那样被调用的对象而不是在构造函数中时,在实现的最深部分(调用保存的函数的位置)中会出现错误。
这个小课可以说明问题:
template<class Signature>
class myfunc{
public:
template<class Func>
myfunc(Func a_func){
// ...
}
};
现在,当编译器搜索重载集的有效函数时,如果不存在完美拟合函数,它会尝试转换参数。转换可以通过函数参数的构造函数发生,也可以通过赋予函数的参数的转换运算符发生。在我们的例子中,它是前者
编译器尝试a
的第一次重载。为了使其可行,它需要进行转换。要将int(*)()
转换为myfunc<int()>
,它会尝试myfunc
的构造函数。作为一个需要任何东西的模板,转换自然会成功
现在它尝试与第二次重载相同。构造函数仍然是相同的,仍然采取任何给予它,转换也有效
在重载集中留下了2个函数,编译器是一个悲伤的熊猫,不知道该怎么做,所以它只是说这个调用很不好。
所以最后,模板的Signature
部分在进行声明/定义时确实属于该类型,但是当你想构造一个对象时则不然。
修改强>:
我全神贯注地回答标题问题,我完全忘记了你的第二个问题。 :(
我可以规避它还是必须保留(讨厌的)显式演员?
Afaik,你有3个选择。
制作相应类型的function
对象并传递
function<int()> fx = x;
function<int(int)> fy = y;
a(fx);
a(fy);
隐藏功能中的繁琐演员并使用TMP获得正确的签名
TMP(模板元编程)版本非常详细,并且带有样板代码,但它隐藏了来自客户端的强制转换。可以找到一个示例版本here,它依赖于部分专门用于函数指针类型的get_signature
元函数(并提供了一个很好的例子,模式匹配在C ++中如何工作):
template<class F>
struct get_signature;
template<class R>
struct get_signature<R(*)()>{
typedef R type();
};
template<class R, class A1>
struct get_signature<R(*)(A1)>{
typedef R type(A1);
};
当然,这需要针对您想要支持的参数数量进行扩展,但这样做一次,然后隐藏在"get_signature.h"
标头中。 :)
我考虑的另一个选项是立即废弃的是SFINAE,它会引入比TMP版本更多的样板代码。
所以,是的,这是我所知道的选择。希望其中一个适合你。 :)
答案 1 :(得分:11)
我已经看过这个问题太多次了。 libc++现在编译此代码时没有歧义(作为符合标准的扩展名)。
逾期更新
这个“扩展”证明非常受欢迎,它在C ++ 14中被标准化(尽管我个人不负责完成这项工作)。
事后看来,我没有完全正确地获得此扩展。本月早些时候(2015-05-09)委员会在LWG issue 2420投票,有效地改变了 Callable 的定义,以便std::function
有void
返回键入它将忽略包装仿函数的返回类型,但如果其他所有内容都匹配,仍然会认为它是 Callable ,而不是认为它不是 Callable 。
此后C ++ 14调整不会影响此特定示例,因为所涉及的返回类型始终为int
。
答案 2 :(得分:4)
以下是如何在检查其构造函数参数的可调用性的类中包装std::function
的示例:
template<typename> struct check_function;
template<typename R, typename... Args>
struct check_function<R(Args...)>: public std::function<R(Args...)> {
template<typename T,
class = typename std::enable_if<
std::is_same<R, void>::value
|| std::is_convertible<
decltype(std::declval<T>()(std::declval<Args>()...)),
R>::value>::type>
check_function(T &&t): std::function<R(Args...)>(std::forward<T>(t)) { }
};
像这样使用:
int a(check_function<int ()> f) { return f(); }
int a(check_function<int (int)> f) { return f(0); }
int x() { return 22; }
int y(int) { return 44; }
int main() {
a(x);
a(y);
}
请注意,这与函数签名上的重载不完全相同,因为它将convertible参数(和返回)类型视为等效。对于精确重载,这应该有效:
template<typename> struct check_function_exact;
template<typename R, typename... Args>
struct check_function_exact<R(Args...)>: public std::function<R(Args...)> {
template<typename T,
class = typename std::enable_if<
std::is_convertible<T, R(*)(Args...)>::value>::type>
check_function_exact(T &&t): std::function<R(Args...)>(std::forward<T>(t)) { }
};
答案 3 :(得分:2)
std::function<T>
有一个转换ctor,它采用任意类型(即T
以外的其他类型)。当然,在这种情况下,ctor会导致类型不匹配错误,但编译器没有那么远 - 调用是不明确的,因为ctor存在。