【问题标题】:Should I use std::function or a function pointer in C++?我应该在 C++ 中使用 std::function 还是函数指针?
【发布时间】:2014-11-09 00:25:27
【问题描述】:

在 C++ 中实现回调函数时,是否仍使用 C 风格的函数指针:

void (*callbackFunc)(int);

或者我应该使用 std::function:

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

【问题讨论】:

  • 如果回调函数在编译时已知,请考虑使用模板。
  • 实现一个回调函数时,你应该做任何调用者需要的事情。如果您的问题真的是关于设计回调接口,那么这里没有足够的信息来回答它。您希望回电的收件人做什么?您需要将哪些信息传递给收件人?接听者应在通话后将哪些信息回传给您?

标签: c++ function c++11 callback std


【解决方案1】:

简而言之,请使用std::function,除非您有理由不这样做。

函数指针的缺点是无法捕捉某些上下文。例如,您将无法将 lambda 函数作为捕获一些上下文变量的回调传递(但如果它不捕获任何变量,它将起作用)。因此也无法调用对象的成员变量(即非静态),因为需要捕获对象 (this-pointer)。(1)

std::function(C++11 起)主要用于存储一个函数(传递它不需要存储它)。因此,如果您想将回调存储在成员变量中,这可能是您的最佳选择。但是,如果您不存储它,它也是一个很好的“首选”,尽管它的缺点是在调用时会引入一些(非常小的)开销(因此在性能非常关键的情况下,这可能是一个问题,但在大多数情况下它不应该)。它非常“通用”:如果您非常关心一致和可读的代码,并且不想考虑您所做的每一个选择(即希望保持简单),请为您传递的每个函数使用 std::function .

考虑第三种选择:如果您要实现一个小函数,然后通过提供的回调函数报告某些内容,请考虑一个 模板参数,然后它可以是任何可调用的对象,即函数指针、仿函数、lambda、std::function、... 缺点是您的(外部)函数成为模板,因此需要在标头中实现。另一方面,您可以获得对回调的调用可以内联的优势,因为您的(外部)函数的客户端代码“看到”对回调的调用将获得可用的确切类型信息。

带有模板参数的版本示例(对于 pre-C++11 写 &amp; 而不是 &amp;&amp;):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

如下表所示,它们各有优缺点:

function ptr std::function template param
can capture context variables no1 yes yes
no call overhead (see comments) yes no yes
can be inlined (see comments) no no yes
can be stored in a class member yes yes no2
can be implemented outside of header yes yes no
supported without C++11 standard yes no3 yes
nicely readable (my opinion) no yes (yes)

(1) 存在解决此限制的解决方法,例如将附加数据作为进一步参数传递给您的(外部)函数:myFunction(..., callback, data) 将调用callback(data)。这就是 C 风格的“带参数的回调”,这在 C++ 中是可能的(顺便说一句,在 WIN32 API 中大量使用)但应该避免,因为我们在 C++ 中有更好的选择。

(2) 除非我们讨论的是类模板,即存储函数的类是模板。但这意味着在客户端,函数的类型决定了存储回调的对象的类型,这几乎不是实际用例的选项。

(3) 对于pre-C++11,使用boost::function

【讨论】:

  • 与模板参数相比,函数指针具有调用开销。模板参数使内联变得容易,即使你被传递到更高的级别,因为正在执行的代码是由参数的类型而不是值来描述的。并且将模板函数对象存储在模板返回类型中是一种常见且有用的模式(通过良好的复制构造函数,您可以创建高效的模板函数可调用,如果需要存储它可以转换为std::function 类型擦除的函数)在立即调用的上下文之外)。
  • @tohecz 我现在提到它是否需要 C++11。
  • @Yakk 哦,当然,忘了!已添加,谢谢。
  • @MooingDuck 当然这取决于实现。但是,如果我没记错的话,由于类型擦除的工作原理,还会发生另一种间接方式吗?但现在我再想一想,我想如果你给它分配函数指针或无捕获的 lambdas,情况就不是这样了......(作为典型的优化)
  • @leemes:对,对于函数指针或无捕获 lambda,它应该具有与 c-func-ptr 相同的开销。这仍然是一个管道停顿+不是简单内联。
【解决方案2】:

void (*callbackFunc)(int); 可能是一个 C 风格的回调函数,但它是一个糟糕的设计,非常不可用。

精心设计的 C 风格回调看起来像 void (*callbackFunc)(void*, int); -- 它有一个 void* 以允许执行回调的代码在函数之外保持状态。不这样做会强制调用者全局存储状态,这是不礼貌的。

std::function&lt; int(int) &gt; 在大多数实现中最终比int(*)(void*, int) 调用更昂贵。然而,对于某些编译器来说,内联更加困难。有std::function 克隆实现可以与函数指针调用开销相媲美(请参阅“最快的委托”等),这些实现可能会进入库。

现在,回调系统的客户端通常需要在创建和删除回调时设置资源并处理它们,并了解回调的生命周期。 void(*callback)(void*, int) 不提供这个。

有时这可以通过代码结构(回调的生命周期有限)或通过其他机制(注销回调等)获得。

std::function 提供了一种有限生命周期管理的方法(对象的最后一个副本在被遗忘时消失)。

一般来说,我会使用std::function,除非出现性能问题。如果他们这样做了,我会首先寻找结构变化(而不是每像素回调,如何根据您传递给我的 lambda 生成扫描线处理器?这应该足以将函数调用开销减少到微不足道的水平。 )。然后,如果它仍然存在,我会根据可能的最快委托写一个delegate,看看性能问题是否消失。

我主要只将函数指针用于遗留 API,或者用于创建 C 接口以在不同编译器生成的代码之间进行通信。当我实现跳转表、类型擦除等时,我还将它们用作内部实现细节:当我同时生产和使用它,并且没有将它暴露在外部以供任何客户端代码使用时,函数指针可以满足我的所有需要.

请注意,您可以编写将 std::function&lt;int(int)&gt; 转换为 int(void*,int) 样式回调的包装器,假设有适当的回调生命周期管理基础架构。因此,作为任何 C 风格回调生命周期管理系统的冒烟测试,我会确保包装 std::function 工作得相当好。

【讨论】:

  • 这个void* 是从哪里来的?为什么要在函数之外保持状态?一个函数应该包含它需要的所有代码、所有功能,你只需将所需的参数传递给它并修改并返回一些东西。如果您需要一些外部状态,那么为什么 functionPtr 或回调会携带那个行李呢?我认为回调是不必要的复杂。
  • @nik-lz 我不确定如何在评论中教你 C 中回调的使用和历史。或者与函数式编程相反的过程哲学。所以,你会留下未完成的。
  • 我忘记了this。是不是因为必须考虑调用成员函数的情况,所以我们需要this 指针来指向对象的地址?如果我错了,你能给我一个链接,我可以在哪里找到更多关于这方面的信息,因为我找不到太多关于它的信息。提前致谢。
  • @Nik-Lz 成员函数不是函数。函数没有(运行时)状态。回调采用void* 来允许传输运行时状态。带有void*void* 参数的函数指针可以模拟对对象的成员函数调用。抱歉,我不知道有什么资源可以介绍“设计 C 回调机制 101”。
  • 是的,这就是我所说的。运行时状态基本上是被调用对象的地址(因为它在运行之间发生变化)。它仍然是关于this。我正是这个意思。好的,谢谢。
【解决方案3】:

使用std::function 存储任意可调用对象。它允许用户提供回调所需的任何上下文;普通函数指针不会。

如果您出于某种原因确实需要使用普通函数指针(可能是因为您想要一个与 C 兼容的 API),那么您应该添加一个 void * user_context 参数,以便它至少可以(尽管不方便)访问状态这不是直接传递给函数的。

【讨论】:

  • 这里的 p 类型是什么?会是 std::function 类型吗?无效 f(){};自动 p = f; p();
【解决方案4】:

避免使用std::function 的唯一原因是支持不支持此模板的旧版编译器,该模板已在 C++11 中引入。

如果不要求支持 C++11 之前的语言,使用 std::function 可以让调用者在实现回调时有更多选择,与“普通”函数指针相比​​,它是一个更好的选择。它为您的 API 的用户提供了更多选择,同时为执行回调的代码抽象出他们的实现细节。

【讨论】:

    【解决方案5】:

    std::function在某些情况下可能会给代码带来VMT,对性能有一定的影响。

    【讨论】:

    • 你能解释一下这个 VMT 是什么吗?
    • 虚法表?
    【解决方案6】:

    其他答案的答案基于技术优点。我会根据经验给你答案。

    作为一个非常重的 X-Windows 开发人员,总是使用带有 void* pvUserData 参数的函数指针回调,我开始使用 std::function 时有些不安。

    但我发现,结合 lambda 等的强大功能,它极大地解放了我的工作,能够随心所欲地输入多个参数,重新排序,忽略调用者想要的参数供应,但我不需要,等等。它确实让开发感觉更松散,反应更灵敏,节省了我的时间,并增加了清晰度。

    在此基础上,我建议任何人在他们通常有回调的任何时候尝试使用std::function。到处尝试,大概六个月,你可能会发现你讨厌回去的想法。

    是的,会有一些轻微的性能损失,但我编写了高性能代码,我愿意为此付出代价。作为一个练习,您可以自己计时,并尝试确定性能差异是否会影响您的计算机、编译器和应用程序空间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-03-20
      • 2017-08-27
      • 1970-01-01
      • 2011-06-13
      • 2014-11-30
      • 2021-12-22
      • 2013-05-07
      相关资源
      最近更新 更多