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

7

在过去,当我想要一个回调函数参数时,我通常决定使用 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<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
                   });

  // [...]
}

尽管这种方法运行良好,但当实现调用函数对象的方法时,我注意到我只是推迟了未知/缺失类型的问题。据我所知,将 void* 强制转换为函数指针或类似的黑科技应该会产生 UB - 编译器怎么知道我实际上正在尝试调用完全未知的类的 operator()?
我考虑将(复制的)函数对象与指向其 operator() 定义的函数指针一起存储,但我不知道如何将函数对象注入函数作为 this,没有它,我怀疑捕获也不能正常工作。
我另一个想法是声明一个纯虚接口,它声明所需的 operator() 函数。不幸的是,我的编译器禁止我将我的函数对象强制转换为接口,并且我认为让 lambda 从接口派生也没有合法的方法。
那么,是否有一种方法可以解决这个问题,或者我可能误用了模板要求/概念功能?

9
在我看来,你遇到的问题恰好是std::function旨在解决的。 - Kevin
4
如果你必须要存储它们,那么为了让它工作,你就得删除类型,并使用std::function。在这种情况下,这不是不必要的开销,因为你确实需要它。 - Barry
5
在这种情况下,开销在哪里?有没有一种方法可以实现function及其所有行为(即能够存储任意可复制的可调用对象),而不需要上述开销?如果有,那么这不是“开销”;这只是拥有该功能的代价。你要么满足于将接口限制为仅函数指针,要么不满足。如果你不满足,那么使其成为可能的代价不是“开销”。 - Nicol Bolas
@ Kevin 我想你是正确的 :/ - Link64
2
@SergeyA:如果这个替代实现提供了std::function 100%的接口,那么问题在于你的标准库对std::function实现,而不是类型本身。 - Nicol Bolas
显示剩余6条评论
1个回答

15

你最初的版本使用 std::function,恰好是因为它擦除了类型。如果你想要类型擦除(而且显然你确实希望如此,因为你希望用户能够使用任何类型,而不需要你的代码明确知道那个类型是什么),那么你需要某种形式的类型擦除。但类型擦除并不是免费的。

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

对于必须超出提供程序调用堆栈生存期的回调,std::function 的开销基本上就是你所需的。也就是说,“开销”不是无意义的;它允许你在回调处理器中存储任意、未知类型的对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接