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

Sod*_*hty 12 c++ lambda constructor overloading c++11

我正在尝试编写一个带有重载构造函数的类,它接受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
Run Code Online (Sandbox Code Playgroud)

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

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

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

Luc*_*ton 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)); }
};
Run Code Online (Sandbox Code Playgroud)

在这种直截了当的情况下,存储特定签名的函子.在那种情况下,我的建议似乎很糟糕,不是吗?怎么可能出错?喜欢的东西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)); }
};
Run Code Online (Sandbox Code Playgroud)

问题在于,简单的事情queue.add([] {})只保证适用于C++ 14 - 在C++ 11中,允许编译器拒绝代码.(最近足够的libstdc ++和libc ++是两个在这方面已经遵循C++ 14的实现.)event_queue::event e = [] {}; queue.add(e);尽管如此仍然可行!所以,只要您使用C++编写代码14,就可以使用它.

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

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

// Boom
f([] { return 4; });
Run Code Online (Sandbox Code Playgroud)

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

有时std::function与模板演绎一起使用,如本问题那个问题所示.这往往会增加一层痛苦和艰辛.


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

我个人有一大堆特征可以检查一个类型是否可以根据签名调用.结合表达EnableIfRequires条款,我仍然可以保持可接受的可读界面.反过来,结合一些排名的重载,我可以实现这样的逻辑:"如果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>);
};
Run Code Online (Sandbox Code Playgroud)

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


Yak*_*ont 2

因此有很多方法可以解决这个问题,这需要不同的工作量。它们都不是完全微不足道的。

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

然后检查签名等效性。

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

template<typename Signature, typename F>
struct call_compatible;
Run Code Online (Sandbox Code Playgroud)

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

现在,如果您重载的两个签名兼容,则需要完成更多工作。即,想象一下,如果您有一个std::function<void(double)>并且std::function<void(int)>- 它们是交叉调用兼容的。

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

如果我们使用该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
  }
};
Run Code Online (Sandbox Code Playgroud)

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

这是第一次尝试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 {};
Run Code Online (Sandbox Code Playgroud)

尚未测试/未编译。

  • 如果您沿着这条路线走下去(检查`T::operator()`*喘气*亵渎!)请记住,您将对任何和所有多态函数对象说不。 (3认同)

归档时间:

查看次数:

644 次

最近记录:

10 年,11 月 前