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

时间:2014-09-15 12:59:02

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

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

void (*callbackFunc)(int);

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

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

6 个答案:

答案 0 :(得分:152)

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

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

std::function(因为C ++ 11)主要是存储一个函数(传递它并不需要存储它)。因此,如果您想将回调存储在例如成员变量中,那么它可能是您的最佳选择。但是如果你不存储它,它是一个很好的第一选择&#34;虽然它的缺点是在被调用时引入一些(非常小的)开销(因此在一个非常严重的性能情况下它可能是一个问题,但在大多数情况下它不应该)。这是非常普遍的&#34;:如果您非常关注一致且可读的代码,并且不想考虑您做出的每一个选择(即想要保持简单),请使用{{ 1}}用于传递的每个功能。

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

带有模板参数的版本的示例(对于预C ++ 11,请写std::function而不是&):

&&

如下表所示,所有这些都有其优点和缺点:

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

(1)存在解决此限制的变通方法,例如将其他数据作为其他参数传递给您的(外部)函数:+-------------------+--------------+---------------+----------------+ | | 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) | | | +-------------------+--------------+---------------+----------------+ 将调用myFunction(..., callback, data)。这是带有参数&#34;的C风格&#34;回调,这在C ++中是可能的(并且通过WIN32 API中大量使用的方式),但是应该避免,因为我们在C ++中有更好的选择。 /子>

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

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

答案 1 :(得分:23)

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

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

在大多数实现中,

std::function< int(int) >最终比int(*)(void*, int) invokation贵一些。然而,一些编译器更难以内联。有std::function克隆实现可以与函数指针调用开销(参见最快可能的代理等)相媲美,这些开销可能会进入库中。

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

有时这可以通过代码结构(回调具有有限的生命周期)或通过其他机制(取消注册回调等)来实现。

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

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

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

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

答案 2 :(得分:17)

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

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

答案 3 :(得分:14)

避免std::function的唯一理由是支持那些不支持此模板的遗留编译器,这在C ++ 11中已经引入。

如果不需要支持pre-C ++ 11语言,使用std::function可以让您的呼叫者在实现回调时有更多选择,与&#34; plain&#34;相比,这是一个更好的选择。函数指针。它为您的API用户提供了更多选择,同时为执行回调的代码抽象出其实现细节。

答案 4 :(得分:0)

在某些情况下,

std::function可能会将VMT带入代码,这会对性能产生一些影响。

答案 5 :(得分:0)

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

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

但我发现结合 lambdas 等的强大功能,它大大解放了我的工作,能够一时兴起,将多个参数放入其中,重新排序它们,忽略调用者想要的参数供应,但我不需要,等等。它确实让开发感觉更松散,响应更快,节省了我的时间,并增加了清晰度。

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

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