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


141

在C ++中实现回调函数时,我仍应使用C样式函数指针:

void (*callbackFunc)(int);

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

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

9
如果在编译时已知回调函数,请考虑使用模板。
Baum mit Augen

4
实现回调函数时,您应该执行调用者需要执行的所有操作。如果您的问题确实是关于设计回调接口的,那么这里没有足够的信息来回答它。您希望回调的接收者做什么?您需要将什么信息传递给收件人?通话后,收件人应将哪些信息传回给您?
Pete Becker 2014年

Answers:


171

简而言之,std::function除非有理由不要使用。

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

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

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

具有模板参数的版本的示例(对于C ++ 11之前的版本,请写&而不是&&):

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

正如您在下表中看到的,它们都有优点和缺点:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1)存在克服此限制的解决方法,例如,将其他数据作为进一步的参数传递给您的(外部)函数:myFunction(..., callback, data)will call callback(data)。这就是C样式的“带参数的回调”,这在C ++中是可行的(并且在WIN32 API中大量使用),但是应避免使用,因为我们在C ++中有更好的选择。

(2)除非我们在谈论类模板,否则存储函数的类就是模板。但这意味着在客户端,函数的类型决定了存储回调的对象的类型,对于实际用例而言,这几乎不是一个选择。

(3)对于C ++ 11之前的版本,请使用 boost::function


9
与模板参数相比,函数指针具有调用开销。模板参数使内联变得容易,即使您向下传递了13个级别,因为正在执行的代码都是由参数的类型而不是值来描述的。而且以模板返回类型存储的模板函数对象是一种常见且有用的模式(有了良好的复制构造函数,您可以创建高效的可调用的模板函数,std::function如果需要将其转换为类型擦除的对象,则可以将其转换为类型擦除的模板函数。称为上下文)。
Yakk-Adam Nevraumont 2014年

1
@tohecz我现在提到是否需要C ++ 11。
leemes 2014年

1
@Y牛哦,当然,忘了!添加了它,谢谢。
leemes

1
@MooingDuck当然取决于实现。但是,如果我没记错的话,由于类型擦除的工作原理,又发生了一种间接寻址?但是,现在我再考虑一下,我想如果您为其分配函数指针或无捕获的lambda,则不是这种情况……(作为典型的优化)
leemes

1
@leemes:对,对于函数指针或不捕获的lambda,它应该具有与c-func-ptr相同的开销。仍然是管道停滞+未被内联。
Mooing Duck

25

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除非表现出对性能的关注,否则我将使用a 。如果是这样,我首先要寻找结构上的变化(而不是每个像素的回调,如何根据传递给我的lambda生成扫描线处理器?这应该足以将函数调用开销减少到微不足道的水平。 )。然后,如果问题仍然存在,我将delegate根据可能最快的代表编写一个基础,看看性能问题是否消失。

我通常只会将函数指针用于旧版API,或用于创建C接口以在不同编译器生成的代码之间进行通信。在实现跳转表,类型擦除等操作时,我也将它们用作内部实现细节:当我同时生产和使用它时,并且没有在外部公开它以供任何客户端代码使用,而函数指针满足了我的全部需求。

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


1
void*是从哪里来的?您为什么要保持功能之外的状态?函数应该包含它需要的所有代码,所有功能,您只需向其传递所需的参数,然后进行修改并返回某些内容。如果您需要一些外部状态,那为什么functionPtr或callback会携带这些行李呢?我认为回调是不必要的复杂。
Nikos

@ nik-lz我不确定如何在注释中教您如何使用C的回调和历史记录。还是与函数式编程相对的过程哲学。因此,您将无法实现。
Yakk-Adam Nevraumont

我忘了this。是否因为必须考虑调用成员函数的情况,所以我们需要this指针指向对象的地址?如果我错了,您能否给我一个链接,在该链接中可以找到更多有关此的信息,因为我对此没有太多了解。提前致谢。
Nikos

@ Nik-Lz成员函数不是函数。函数没有(运行时)状态。回调采用void*来允许传输运行时状态。具有void*void*参数的函数指针可以模拟对对象的成员函数调用。抱歉,我不了解“设计C回调机制101”中涉及的资源。
Yakk-Adam Nevraumont

是的,那是我在说的。运行时状态基本上是被调用对象的地址(因为它在运行之间会发生变化)。还在this。我正是这个意思。好的,谢谢。
Nikos

17

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

如果由于某些原因(也许是因为您想要C兼容的API)而确实需要使用普通函数指针,则应添加一个void * user_context参数,这样至少可以(尽管很不方便)访问不直接传递给参数的状态。功能。


这里的p是什么类型?它会是std :: function类型吗?无效f(){}; 自动p = f; p();
sree

14

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

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


1

std::function 在某些情况下可能会将VMT引入代码中,从而对性能产生一些影响。


3
您能解释一下这个VMT是什么吗?
Gupta
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.