使用 C++20 概念来避免 std::function

Lin*_*k64 6 c++ lambda c++-concepts c++20

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

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

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

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

阅读有关新的 C++ 20 特性的内容时,我认为我可以利用概念来避免std::function对任何可行的函子使用和使用受约束的参数。

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

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
                   });

  // [...]
}
Run Code Online (Sandbox Code Playgroud)

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

我想过将(复制的)函子与指向它的operator()定义的函数指针一起存储,但是我不知道如何将函子注入到函数this内部,如果没有它,我怀疑捕获会起作用。

我采用的另一种方法是声明一个纯虚拟接口来声明所需的operator()功能。不幸的是,我的编译器禁止我将函子转换为接口,而且我认为也不存在让 lambda 从中派生的合法方法。

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

Nic*_*las 13

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

约束是针对模板的。你不想要的模板功能; 您需要一个处理类型擦除的可调用函数的函数。

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