40

给定以下代码,歧义背后的原因是什么?我可以绕过它还是必须保留(烦人的)显式演员表?

#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()函数并在我的 main 中调用,编译会因为类型不匹配和唯一可用函数的参数而正确失败。如果编译器在这种情况下失败,为什么存在两个函数时会有任何歧义?function<int ()>a(x)xfunction<int (int)>a()a()

我已经尝试过使用 VS2010 和 g++ v. 4.5。两者都给了我完全相同的歧义。

4

4 回答 4

42

问题是两者function<int()>function<int(int)>都可以从同一个函数构造。这是std::functionVS2010 中的构造函数声明:

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. 为了使其可行,它需要进行转换。要将 a 转换int(*)()为 a myfunc<int()>,它会尝试 的构造函数myfunc。作为一个可以接受任何东西的模板,转换自然会成功。
现在它尝试与第二个重载相同。构造函数仍然是相同的并且仍然接受任何给它的东西,转换也有效。
在重载集中留下了 2 个函数,编译器是一只悲伤的熊猫,不知道该做什么,所以它只是说这个调用是模棱两可的。


所以最后,Signature模板的部分在进行声明/定义时确实属于类型,但在您要构造对象时不属于。


编辑
我全神贯注地回答标题问题,我完全忘记了你的第二个问题。:(

我可以绕过它还是必须保留(烦人的)显式演员表?

Afaik,您有 3 个选项。

  • 保留演员阵容
  • 制作一个function适当类型的对象并传递它

    function<int()> fx = x; function<int(int)> fy = y; a(fx); a(fy);

  • 隐藏函数中繁琐的转换并使用 TMP 获得正确的签名

TMP(模板元编程)版本非常冗长并且带有样板代码,但它对客户端隐藏了强制转换。可以在这里找到一个示例版本,它依赖于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 版本更多的样板代码。

所以,是的,这就是我所知道的选项。希望其中一个对你有用。:)

于 2011-05-09T00:26:23.620 回答
11

我已经看到这个问题出现了太多次了。 libc++现在可以毫无歧义地编译此代码(作为符合标准的扩展)。

逾期更新

这个“扩展”被证明非常流行,以至于它在 C++14 中被标准化(尽管我个人不负责完成这项工作)。

事后看来,我没有得到这个扩展完全正确。本月早些时候 (2015-05-09) 委员会在LWG 问题 2420中投票有效地改变了Callable的定义,因此如果它std::function有一个void返回类型,它将忽略包装函子的返回类型,但仍然认为它是Callable if其他一切都匹配,而不是认为它不是Callable

这个 C++14 后的调整不会影响这个特定的例子,因为所涉及的返回类型是一致的int

于 2011-05-31T23:16:32.277 回答
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);
}

请注意,这与函数签名上的重载并不完全相同,因为它将可转换参数(和返回)类型视为等效。对于精确的重载,这应该有效:

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)) { }
};
于 2012-08-21T12:08:46.617 回答
2

std::function<T>有一个转换 ctor,它采用任意类型(即 a 以外的东西T)。当然,在这种情况下,该 ctor 会导致类型不匹配错误,但编译器并没有走得那么远——调用是模棱两可的,仅仅是因为 ctor 存在。

于 2011-05-09T00:36:45.390 回答