如何在std :: function的签名上重载构造函数?

时间:2013-05-11 00:11:59

标签: c++ c++11 constructor lambda overloading

我正在尝试编写一个带有重载构造函数的类,它接受std :: function对象作为参数,但当然每个该死的东西都可以隐式地转换为任何签名的std :: function 。这自然是非常有用的。

示例:

class Foo {
  Foo( std::function<void()> fn ) {
    ...code...
  }
  Foo( std::function<int()> fn ) {
    ...code...
  }
};

Foo a( []() -> void { return; } );    // Calls first constructor
Foo b( []() -> int { return 1; } );   // Calls second constructor

这不会编译,抱怨两个构造函数基本相同且含糊不清。当然,这是无稽之谈。我尝试过enable_if,is_same和其他一些东西。接受函数指针是不可能的,因为这会阻止有状态lambda的传递。当然必须有办法实现这个目标吗?

我的模板技能有点缺乏,我很害怕。普通的模板类和函数很容易,但使用编译器玩傻傻的玩家有点超出了我的联盟。有人可以帮帮我吗?

我之前已经知道这个问题的变体,但它们通常关注的是正常函数而不是构造函数;或者通过参数而不是返回类型重载。

3 个答案:

答案 0 :(得分:5)

以下是一些常见情况,以及为什么我认为std::function不适合他们:

struct event_queue {
    using event = std::function<void()>;
    std::vector<event> events;

    void add(event e)
    { events.emplace_back(std::move(e)); }
};

在这种直截了当的情况下,存储特定签名的仿函数。在那种情况下,我的建议似乎很糟糕,不是吗?怎么可能出错?像queue.add([foo, &bar] { foo.(bar, baz); })这样的东西运行良好,类型擦除正是你想要的功能,因为可能会存储异构类型的函子,因此它的成本不是问题。事实上,这可以说是std::function<void()>签名中使用add可以接受的一种情况。但请继续阅读!

在未来的某个时刻,您会发现某些事件在被召回时可能会使用某些信息 - 所以您尝试:

struct event_queue {
    using event = std::function<void()>;
    // context_type is the useful information we can optionally
    // provide to events
    using rich_event = std::function<void(context_type)>;
    std::vector<event> events;
    std::vector<rich_event> rich_events;

    void add(event e) { events.emplace_back(std::move(e)); }
    void add(rich_event e) { rich_events.emplace_back(std::move(e)); }
};

问题在于,queue.add([] {})这样简单的东西只能保证适用于C ++ 14 - 在C ++ 11中,允许编译器拒绝代码。 (最近,libstdc ++和libc ++是两个在这方面已经遵循C ++ 14的实现。)event_queue::event e = [] {}; queue.add(e);之类的东西仍然有效!因此,只要您使用C ++编写代码,就可以使用它。

但是,即使使用C ++ 14,std::function<Sig>的此功能也可能并不总能满足您的需求。请考虑以下内容,它现在无效,也将在C ++ 14中使用:

void f(std::function<int()>);
void f(std::function<void()>);

// Boom
f([] { return 4; });

也有充分的理由:std::function<void()> f = [] { return 4; };不是错误,工作正常。返回值被忽略并被遗忘。

有时std::function与模板扣除同时使用,如this questionthat one所示。这往往会增加一层痛苦和艰辛。


简而言之,std::function<Sig>未在标准库中进行特殊处理。它仍然是用户定义的类型(在某种意义上它与int不同),它遵循正常的重载分辨率,转换和模板推导规则。这些规则非常复杂并且彼此交互 - 它不是对接口用户的服务,他们必须记住这些以将可调用对象传递给它。 std::function<Sig>具有这种悲剧性的诱惑,它看起来有助于使界面简洁,更具可读性,但只要你不重载这样的界面,它就真的只有。

我个人有很多特征可以检查类型是否可以根据签名调用。结合expressive EnableIf or Requires clauses我仍然可以保持一个可接受的可读界面。反过来,结合一些ranked overloads,我可以实现这样的逻辑:“如果functor在没有参数的情况下调用时可以转换为int,或者在此情况下回退到这个过载,则调用此重载”。这看起来像是:

class Foo {
public:
    // assuming is_callable<F, int()> is a subset of
    // is_callable<F, void()>
    template<typename Functor,
             Requires<is_callable<Functor, void()>>...>
    Foo(Functor f)
        : Foo(std::move(f), select_overload {})
    {}

private:
    // assuming choice<0> is preferred over choice<1> by
    // overload resolution

    template<typename Functor,
             EnableIf<is_callable<Functor, int()>>...>
    Foo(Functor f, choice<0>);
    template<typename Functor,
             EnableIf<is_callable<Functor, void()>>...>
    Foo(Functor f, choice<1>);
};

请注意,is_callable精神中的特征会检查给定的签名 - 也就是说,它们检查某些给定的参数和一些预期的返回类型。它们不进行内省,因此它们在例如面前表现良好。重载函子。

答案 1 :(得分:2)

所以有很多方法可以解决这个问题,这需要付出不同的工作量。它们都不是完全无足轻重的。

首先,您可以通过检查T::operator()和/或检查它是否为R (*)(Args...)类型来解压缩传入类型的签名。

然后检查签名的等效性。

第二种方法是检测呼叫兼容性。写一个这样的特征类:

template<typename Signature, typename F>
struct call_compatible;

可以是std::true_typestd::false_type,具体取决于decltype<F&>()( declval<Args>()... )是否可转换为Signature返回值。在这种情况下,这将解决您的问题。

现在,如果您正在重载的两个签名兼容,则需要完成更多工作。即,假设您有std::function<void(double)>std::function<void(int)> - 它们是交叉呼叫兼容的。

要确定哪个是“最佳”,您可以在我之前的问题中查看here,我们可以在这里查看一堆签名并找出哪个匹配最佳。然后进行返回类型检查。这变得越来越复杂了!

如果我们使用call_compatible解决方案,您最终会做的是:

template<size_t>
struct unique_type { enum class type {}; };
template<bool b, size_t n=0>
using EnableIf = typename std::enable_if<b, typename unique_type<n>::type>::type;

class Foo {
  template<typename Lambda, EnableIf<
    call_compatible<void(), Lambda>::value
    , 0
  >...>
  Foo( Lambda&& f ) {
    std::function<void()> fn = f;
    // code
  }
  template<typename Lambda, EnableIf<
    call_compatible<int(), Lambda>::value
    , 1
  >...>
  Foo( Lambda&& f ) {
    std::function<int()> fn = f;
    // code
  }
};

这是其他解决方案的一般模式。

这是call_compatible的第一次尝试:

template<typename Sig, typename F, typename=void>
struct call_compatible:std::false_type {};

template<typename R, typename...Args, typename F>
struct call_compatible<R(Args...), F, typename std::enable_if<
  std::is_convertible<
    decltype(
      std::declval<F&>()( std::declval<Args>()... )
    )
    , R
  >::value
>::type>:std::true_type {};

尚未经过测试/未编译。

答案 2 :(得分:0)

所以我有一个全新的解决方案来解决这个问题,它在MSVC 2013中有效,并且不会吮吸(比如查看指向operator()的指针)。

标签发送它。

可以携带任何类型的标签:

template<class T> struct tag{using type=T;};

一个接受类型表达式并生成类型标记的函数:

template<class CallExpr>
tag<typename std::result_of<CallExpr>::type>
result_of_f( tag<CallExpr>={} ) { return {}; }

并使用:

class Foo {
private:
  Foo( std::function<void()> fn, tag<void> ) {
    ...code...
  }
  Foo( std::function<int()> fn, tag<int> ) {
    ...code...
  }
public:
  template<class F>
  Foo( F&& f ):Foo( std::forward<F>(f), result_of_f<F&()>() ) {}
};

现在Foo(something)转发到std::function<int()>std::function<void()>构造函数,具体取决于上下文。

如果您愿意,可以通过添加ctor来支持转换,从而使tag<>变得更聪明。然后,返回double的函数将调度到tag<int>

template<class T> struct tag{
  using type=T;
  tag(tag const&)=default;
  tag()=default;
  template<class U,
    class=typename std::enable_if<std::is_convertible<U,T>::value>::type
  >
  tag( tag<U> ){}
};

请注意,这不支持类似SFINAE Foo - 构造失败。也就是说,如果你将int传递给它,它将会失败,而不是软失败。

虽然这在VS2012中不能直接使用,但您可以转发到构造函数体中的初始化函数。