【问题标题】:Using C++20 concepts to avoid std::function使用 C++20 概念来避免 std::function
【发布时间】:2020-12-11 02:40:59
【问题描述】:

过去,当我想要一个回调作为函数参数时,我通常决定使用std::function。在我绝对从不使用捕获的极少数情况下,我使用typedef 代替函数声明。

所以,通常我的带有回调参数的声明看起来像这样:

struct Socket
{
  void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}

但是,据我所知,std::function 实际上在运行时做了一些工作,因为必须将 lambda 及其捕获解析到 std::function 模板并移动/复制它的捕获 (?)。

阅读 C++ 20 的新特性后,我想我或许可以利用概念来避免使用 std::function,并为任何可行的函子使用受约束的参数。

这就是我的问题出现的地方:因为我想在将来的某个时候使用回调函子对象,所以我必须存储它们。由于我的回调没有明确的类型,我最初的想法是将仿函数复制(最终在某个时候移动)堆并使用std::vector&lt;void*&gt; 来记录我离开它们的位置。

template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
                       && std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
                       && std::is_copy_constructible_v<Functor>;
struct Socket
{
  std::vector<void*> callbacks;

  template<ReceiveCallback TCallback>
  void on_receive(TCallback const& callback)
  {
    callbacks.push_back(new TCallback(callback));
  }
}

int main(int argc, char** argv)
{
  Socket* sock;
  // [...] inialize socket somehow

  sock->on_receive([](uint8_t* data, unsigned long length)
                   {
                     // NOP for now
                   });

  // [...]
}

虽然这工作得很好,但在实现应该调用函子的方法时,我注意到我刚刚推迟了未知/缺失类型的问题。据我了解,将void* 转换为函数指针或一些类似的hack 应该会产生UB - 编译器怎么知道,我实际上是在尝试调用一个完全未知的类的operator()?

我曾考虑将(复制的)仿函数连同指向它的 operator() 定义的函数指针一起存储,但是我不知道如何将仿函数作为 this 注入函数内部,没有它我怀疑捕获会工作的。

我的另一种方法是声明一个纯虚拟接口,该接口声明所需的operator() 函数。不幸的是,我的编译器禁止我将仿函数转换为接口,我认为也没有合法的方法可以让 lambda 派生自它。

那么,有没有办法解决这个问题,还是我可能只是滥用了模板要求/概念功能?

【问题讨论】:

  • 不清楚您将如何调用回调。您将其转换为void*,但是如何将其转换回可调用类型?或者这是你问题的要点?如果是,答案是'你不能没有虚拟通话,这就是std::function 已经在做的事情。
  • 在我看来,您遇到的问题正是 std::function 要解决的问题。
  • 如果你存储它们,那么你擦除类型以使其工作,所以你 使用某些东西std::function。在这种情况下,这不是不必要的开销,因为您确实需要它。
  • @SergeyA:在这种情况下,开销在哪里?有没有办法实现function 的所有行为(即:能够存储任何任意的、可复制的可调用对象)而没有上述开销?如果是这样,那么它不是“开销”;这只是拥有该功能的成本。要么你可以将你的接口限制为函数指针,要么你不能。如果您不是,那么实现这一目标的成本就不是“开销”。
  • @SergeyA:如果这个替代实现提供了 100% 的std::function 接口,那么这是您的标准库的std::function实现 的问题,而不是类型本身。

标签: c++ lambda c++20 c++-concepts


【解决方案1】:

您的初始版本恰好使用了std::function因为它会删除类型。如果您想要类型擦除(并且您显然这样做,因为您希望用户能够在您的代码不明确知道该类型是什么的情况下使用任何类型),那么您需要某种形式的类型擦除。而且类型擦除不是免费的。

约束用于模板。你不想要模板函数;你想要一个处理类型擦除的可调用对象的函数。

对于必须比提供程序的调用堆栈更长的回调,std::function 的开销几乎是您所需要的。也就是说,“开销”并非毫无意义;它允许您在回调处理器中存储任意未知类型的对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-11-27
    • 2018-05-31
    • 2020-09-28
    • 2021-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多