我应该在C++中使用std::function还是函数指针?

ejk8hzay  于 2023-03-25  发布在  其他
关注(0)|答案(7)|浏览(168)

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

void (*callbackFunc)(int);

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

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

hfsqlsce1#

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

函数指针的缺点是不能捕获一些上下文。例如,你不能将lambda函数作为一个回调函数来捕获一些上下文变量(但如果它不捕获任何上下文变量,它将工作)。因此,调用对象的成员变量(即非静态)也是不可能的,因为对象(this-指针)需要被捕获。(1)
std::function(C11起)主要是存储一个函数(传递它并不需要存储它)。因此,如果你想将回调存储在成员变量中,这可能是你最好的选择。但是如果你不存储它,这是一个很好的“第一选择”,虽然它有缺点,引入一些(非常小)的开销(所以在性能非常关键的情况下,它可能是一个问题,但在大多数情况下它不应该)。它是非常“通用”的:如果你非常关心代码的一致性和可读性,并且不想考虑你所做的每一个选择(即想保持简单),那么对你传递的每一个函数使用std::function
想想第三种选择:如果你要实现一个小函数,然后通过提供的回调函数报告一些东西,考虑一个模板参数,它可以是 * 任何可调用对象 *,即函数指针,函子,lambda,std::function,...这里的缺点是(外部)函数成为模板,因此需要在头部中实现。另一方面,您可以获得对回调的调用可以内联的优点,当你的(外部)函数的客户端代码“看到”回调函数的调用时,它将提供确切的类型信息。
带有模板参数的版本示例(对于pre-C
11,写&而不是&&):

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

从下表中可以看出,它们都有各自的优点和缺点:
| | 函数ptr|标准::函数|模板参数|
| - ------|- ------|- ------|- ------|
| 可以捕获上下文变量|第一|是的|是的|
| 无呼叫开销(参见注解)|是的|没有|是的|
| 可以内联(见注解)|没有|没有|是的|
| 可以存储在类成员中|是的|是的|二号|
| 可在标头外部实现|是的|是的|没有|
| 不支持C11标准|是的|三号|是的|
| 我的观点(My Opinion)|没有|是的|(有)|
(1)解决方法可以克服这个限制,例如将额外的数据作为进一步的参数传递给你的(外部)函数:myFunction(..., callback, data)将调用callback(data)。这是C风格的“带参数的回调”,这在C
中是可能的(顺便说一下,在WIN32 API中大量使用),但应该避免,因为我们在C中有更好的选择。
(2)除非我们讨论的是类模板,即存储函数的类是模板。但这意味着在客户端,函数的类型决定了存储回调的对象的类型,这几乎从来不是实际用例的选项。
(3)对于C
11之前的版本,使用boost::function

bybem2ql

bybem2ql2#

void (*callbackFunc)(int);可能是一个C风格的回调函数,但它是一个糟糕设计的可怕的不可用函数。
一个设计良好的C风格回调看起来像void (*callbackFunc)(void*, int);--它有一个void*,允许执行回调的代码在函数之外维护状态。不这样做会迫使调用者全局存储状态,这是不礼貌的。
在大多数实现中,std::function< int(int) >最终比int(*)(void*, int)调用稍微昂贵一些。然而,对于一些编译器来说,内联更难。有std::function克隆实现可以与函数指针调用开销相媲美(参见“最快的可能委托”等),这些实现可能会进入库。
现在,回调系统的客户端通常需要设置资源,并在创建和删除回调时释放它们,并了解回调的生命周期。
有时这可以通过代码结构(回调具有有限的生命周期)或通过其他机制(注销回调等)来实现。
std::function提供了一种有限生存期管理的方法(对象的最后一个副本在被遗忘时消失)。
一般来说,我会使用std::function,除非出现性能问题。(而不是每个像素的回调,如何生成一个扫描线处理器的基础上的lambda你给我?这应该是足够的,以减少函数调用开销微不足道的水平。)。然后,如果它仍然存在,我会写一个delegate基于最快的可能委托,看看性能问题是否会消失。
我通常只会在遗留API中使用函数指针,或者在创建C接口时使用函数指针,以便在不同编译器生成的代码之间进行通信。当我实现跳转表,类型擦除等时,我也使用它们作为内部实现细节:当我既生产又消费它,并且不向外部公开它以供任何客户端代码使用时,函数指针就可以满足我的所有需要。
请注意,如果有适当的回调生命周期管理基础设施,您可以编写 Package 器将std::function<int(int)>转换为int(void*,int)样式的回调。因此,作为任何C样式回调生命周期管理系统的冒烟测试,我将确保 Package std::function工作得相当好。

nzk0hqpo

nzk0hqpo3#

使用std::function存储任意可调用对象。它允许用户提供回调所需的任何上下文;普通函数指针则不是。
如果出于某种原因需要使用普通函数指针(也许是因为您想要一个C兼容的API),那么您应该添加一个void * user_context参数,这样它至少可以(尽管不方便)访问未直接传递给函数的状态。

jk9hmnmh

jk9hmnmh4#

避免std::function的唯一原因是支持缺乏此模板支持的遗留编译器,该模板已在C11中引入。
如果不需要支持C
11之前的语言,使用std::function可以让调用者在实现回调时有更多的选择,与“普通”函数指针相比,它是一个更好的选择。它为API的用户提供了更多的选择,同时为执行回调的代码抽象出了他们实现的细节。

mzsu5hc0

mzsu5hc05#

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

uidvcgyl

uidvcgyl6#

其他的答案都是基于技术上的优点,我会根据经验给予你一个答案。
作为一个非常繁重的X-Windows开发人员,总是使用带有void* pvUserData参数的函数指针回调,我开始使用std::function时有些不安。
但我发现,结合lambda表达式等的强大功能,它大大解放了我的工作,能够随心所欲地抛出多个参数,重新排序它们,忽略调用者想要提供但我不需要的参数,等等。
在此基础上,我建议任何人在他们通常有回调的任何时候尝试使用std::function,在任何地方尝试,大约六个月,你可能会发现你讨厌回去的想法。
是的,有一些轻微的性能损失,但我写的是高性能代码,我愿意付出代价。作为一个练习,你自己计时,试着找出性能差异是否会影响你的计算机,编译器和应用程序空间。

d7v8vwbk

d7v8vwbk7#

有一个用例,普通的C函数指针是正确的答案:比较。
std::function没有正确的相等(不相等)概念。唯一支持的比较是与nullptr,也就是'empty'状态进行比较。而函数指针只是指针,所以“==”可以工作,并且做了你通常想要做的事情:如果回调指向完全相同的代码(或两者都为null),则返回true,否则比较为false。因此,如果您真的需要支持比较回调,则需要普通的旧函数指针或自定义函数类型(带有“operator()”的结构体),而不是std::function。

相关问题