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

192

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

void (*callbackFunc)(int);

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

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

12
如果回调函数在编译时已知,考虑使用模板代替。 - Baum mit Augen
5
当实现回调函数时,您应该根据调用者的要求执行相应操作。如果您的问题实际上是关于设计回调接口的,那么这里提供的信息远远不足以回答该问题。您想让回调接收方做什么?您需要传递哪些信息给接收方?接收方在调用后应该向您返回什么信息? - Pete Becker
相关链接:https://dev59.com/Xmox5IYBdhLWcg3wqmBt#9054802 和 https://dev59.com/B14b5IYBdhLWcg3wvT6w - Gabriel Staples
7个回答

222
简而言之,除非你有理由不这样做,否则请使用std::function
函数指针的缺点是无法捕获某些上下文。例如,你无法将一个捕获了一些上下文变量的lambda函数作为回调函数传递(但如果它没有捕获任何变量,则可以正常工作)。因此,也无法调用对象的数据成员(即非静态成员),因为需要捕获对象(即this指针)(1)

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

考虑第三个选项:如果你要实现一个小功能,然后通过提供的回调函数报告一些东西,可以考虑使用模板参数,它可以是任何可调用对象,例如函数指针、仿函数、lambda表达式、std::function等等。缺点是你的(外部)函数将变成一个模板,因此需要在头文件中实现。另一方面,你可以获得的优势是回调函数的调用可以内联,因为你的(外部)函数的客户端代码“看到”了回调函数的调用,并且具有准确的类型信息。
以下是使用模板参数的示例(对于C++11之前的版本,请将&&替换为&):
template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

如您所见,根据下表,它们各自都有优点和缺点:
函数指针 std::function 模板参数
能捕获上下文变量 1
没有调用开销(见注释)
可以内联(见注释)
可以存储在类成员中 2
可以在头文件之外实现
支持非C++11标准 3
易读(个人观点) (是)

(1)存在解决这个限制的方法,例如将附加数据作为进一步参数传递给你的(外部)函数:`myFunction(..., callback, data)` 将调用 `callback(data)`。这是C风格的“带参数回调”,在C++中是可能的(顺便说一句,在WIN32 API中广泛使用),但应该避免使用,因为在C++中有更好的选择。
(2)除非我们讨论的是类模板,即存储函数的类是一个模板。但这意味着在客户端上,函数的类型决定了存储回调的对象的类型,这在实际使用情况下几乎从不是一个选项。
(3)对于C++11之前的版本,请使用boost::function

10
与模板参数相比,函数指针调用会有额外的开销。即使在多级传递的情况下,模板参数可以使内联变得容易,因为执行的代码由参数的类型而非值来描述。将模板函数对象存储在模板返回类型中是一种常见且有用的模式(具有良好的复制构造函数,您可以创建高效的可调用模板函数,如果需要在立即调用上下文之外存储它,则可以将其转换为 std::function 类型擦除)。 - Yakk - Adam Nevraumont
1
@MooingDuck 当然这取决于具体实现。但如果我没记错的话,由于类型擦除的工作方式,会多出一个间接引用?但是现在我再想一想,如果你将函数指针或无捕获 lambda 分配给它,我猜这种情况不会发生...(作为典型的优化)。 - leemes
1
@leemes:对于函数指针或无捕获的lambda,它应该具有与c-func-ptr相同的开销。这仍然是一个流水线停顿+不容易内联的问题。 - Mooing Duck
1
@leemes:被接受的答案中的链接说开销“类似于标准函数指针”,并且“使用正确的内联编译器,对函数对象的调用需要通过一个函数指针进行一次调用。”第二个答案说:“动态内存分配是人们最常提到的std::function所隐含的性能惩罚。” - Mooing Duck
1
我也不喜欢函数指针的那种难以辨认的标准语法。如果您在C++11规则下编译,您可以避免使用那整个声明样式,而是选择例如 using functionptr_f = std::add_pointer_t<bool(void*, std::string const&, int)> - 这将声明一个函数指针,相当于旧语法中的 typedef void (* functionptr_f)(void*, std::string const&, int)。我发现using形式更加易读和表达性强; 它还有一个优点,就是可以通过将 add_pointer_t 替换为 function 来轻松转换为 std :: function 声明。 - fish2000
显示剩余22条评论

34

void (*callbackFunc)(int) 可能是一个C风格的回调函数,但它是设计很差且难以使用的。

一个良好设计的C风格回调函数应该长这样:void (*callbackFunc)(void*, int) -- 它有一个 void* 用于让执行回调的代码在函数之外维护状态。不这样做会强制调用者在全局存储状态,这是不礼貌的。

std::function< int(int) > 在大多数实现中比 int(*)(void*, int) 调用略微更昂贵。然而,对于某些编译器来说,它更难进行内联优化。有一些 std::function 的克隆实现可以与函数指针调用相媲美(参见“最快的代理”等),这些实现可能会进入库中。

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

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

std::function 提供了有限生命周期管理的手段(当最后一个对象被遗忘时它消失)。

总的来说,除非存在性能问题,我会使用 std::function。如果存在性能问题,我首先会寻找结构性的变化(例如,不是逐像素回调,而是基于你传给我的lambda生成扫描线处理器?这应该足以将函数调用开销降到微不足道的水平)。然后,如果问题仍然存在,我会编写一个基于最快代理的委托,并看看性能问题是否得到解决。

我通常只在遗留的API或为不同编译器生成的代码之间创建C接口时使用函数指针。当我需要实现跳转表、类型擦除等内部细节时,我会使用它们:当我既要生产又要消费时,并且不向任何客户端代码公开,函数指针就够用了。

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


1
这个 void* 是从哪里来的?为什么你想要在函数之外维护状态?一个函数应该包含它所需的所有代码和功能,你只需要传递所需的参数并修改和返回一些东西。如果你需要一些外部状态,那么为什么函数指针或回调要承载这个负担呢?我认为回调是不必要复杂的。 - KeyC0de
@Nik-Lz 成员函数不是函数。函数没有(运行时)状态。回调函数采用void*以允许传输运行时状态。带有void*void*参数的函数指针可以模拟对对象的成员函数调用。很抱歉,我不知道有哪些资源可以介绍“设计C回调机制101”。 - Yakk - Adam Nevraumont
是的,这就是我所说的。运行时状态基本上是被调用对象的地址(因为它在运行之间会发生变化)。它仍然涉及到 this。这就是我所指的。好的,无论如何还是谢谢。 - KeyC0de
@Nik-Lz 不是的,它可以携带任意有效负载。 成员函数并没有什么特别之处,它们只是一种隐式第一个参数为指向特定类或结构体的指针的函数。 一个自由函数可以使用指向特定类或结构体或甚至是整数的显式参数来进行调用。您可以传递 void* 指向一个 int 的指针以记录回调被调用的次数,例如: void increment_int(void* ptr){ int* pint = static_cast<int*>(ptr); ++*pint; } 。或者您可以定义一个 struct counter { int x; void increment() { ++x; } }; void counter_increment( void* ptr ) - Yakk - Adam Nevraumont
{ static_cast<counter*>(ptr)->increment(); }并获得相同的结果和语义。void*使其类型不安全,但在通过不具备类型感知能力的C API时,某些类型不安全是不可避免的。 - Yakk - Adam Nevraumont
显示剩余5条评论

20

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

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


这里的p是什么类型?它会是std::function类型吗? void f(){}; auto p = f; p(); - uss

18

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

如果不需要支持C++11之前的语言版本,则与“纯”函数指针相比,使用std::function可以让调用者在实现回调时拥有更多选择,从而成为更好的选项。它为您API的用户提供了更多的选择,同时将其实现的细节抽象出来,使您的执行回调的代码更加通用。


1

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


3
可以,请问"VMT"是什么意思? - TonySalimi
1
虚函数表? - mic

1
其他答案都是基于技术优势的。我会根据经验给你一个答案。作为一个非常重视X-Windows开发的人,我总是使用带有void* pvUserData参数的函数指针回调,因此我开始有些害怕使用std::function。但是我发现,结合lambda等强大的功能,它使我的工作变得更加自由,可以随时抛出多个参数,重新排序它们,忽略来电者想提供但我不需要的参数等等。这真的让开发感觉更加松散和响应,节省了时间,并增加了清晰度。基于这个基础,我建议任何人在通常需要回调的时候都尝试使用std::function。到处尝试,六个月后,你可能会发现你讨厌回去的想法。是的,有一些轻微的性能损失,但我编写高性能代码并且愿意支付代价。作为一项练习,请自己计时并尝试确定性能差异是否会在您的计算机、编译器和应用程序空间中起作用。

0

有一种情况适合使用普通的C函数指针:比较。

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


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