std :: function的模板参数(签名)是不是它的类型?

时间:2011-05-08 23:57:48

标签: c++ c++11 std-function

鉴于以下代码,歧义背后的原因是什么?我可以规避它还是必须保留(讨厌的)明确的演员?

#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。两者都给我完全相同的歧义。

4 个答案:

答案 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::functionvoid返回键入它将忽略包装仿函数的返回类型,但如果其他所有内容都匹配,仍然会认为它是 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存在。