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

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

void (*callbackFunc)(int);

或者我应该使用 std: : 函数:

std::function< void(int) > callbackFunc;
76068 次浏览

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

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

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

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

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

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

std::function(因为 C + + 11)主要是对 商店的一个函数(传递它不需要存储它)。因此,如果您想将回调函数存储在成员变量中,那么这可能是您的最佳选择。但是,如果你不存储它,它是一个很好的“第一选择”,虽然它有缺点,引入一些(非常小)开销时,被调用(所以在一个非常性能关键的情况下,它可能是一个问题,但在大多数情况下,它不应该)。它是非常“通用”的: 如果你非常关心一致性和可读性的代码,并且不想考虑你所做的每个选择(比如想让它保持简单) ,那么对你传递的每个函数使用 std::function

想想第三个选项: 如果你要实现一个小函数,然后通过提供的回调函数报告一些东西,考虑一个 模板参数,然后可以是 任何可调用的对象,也就是一个函数指针,一个函数,一个 lambda,一个 std::function,... 缺点是你的(外部)函数变成了一个模板,因此需要在头部实现。另一方面,对回调函数的调用可以是内联的,因为(外部)函数的客户机代码“看到”对回调函数的调用将获得确切的类型信息。

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

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

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

函数 ptr 函数 ptr 函数 模板参数
可以捕获上下文变量 不行 是的 是的
无通话开销(见注释) 是的 没有 是的
可以内联(见注释) 没有 没有 是的
可以存储在类成员中 是的 是的 不行
可以在头之外实现 是的 是的 没有
不支持 C + + 11标准 是的 不行 是的
易读(我的意见) 没有 是的 (是的)

(1)存在工作区来克服这个限制,例如将额外的数据作为进一步的参数传递给(外部)函数: myFunction(..., callback, data)将调用 callback(data)。这是 C 风格的“带参数的回调”,这在 C + + 中是可能的(在 WIN32 API 中大量使用) ,但是应该避免,因为我们在 C + + 中有更好的选择。

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

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

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

设计良好的 C 样式回调类似于 void (*callbackFunc)(void*, int);——它有一个 void*,允许执行回调的代码维护函数之外的状态。不这样做会迫使调用方将状态存储在全局,这是不礼貌的。

在大多数实现中,std::function< int(int) >最终比 int(*)(void*, int)调用稍微昂贵一些。然而,对于某些编译器来说,内联更加困难。还有一些实现可以与函数指针调用开销(参见“最快可能的委托”等)相抗衡,这些实现可能会进入库。

现在,回调系统的客户端通常需要设置资源,并在创建和删除回调时释放这些资源,同时要了解回调的生命周期。void(*callback)(void*, int)不提供这一点。

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

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

一般来说,除非性能问题明显,否则我会使用 std::function。如果他们这样做了,我会首先寻找结构的变化(而不是每个像素的回调,如何生成一个扫描线处理器的基础上,你传给我的 lambda?这应该足以将函数调用开销减少到微不足道的水平。).然后,如果这种情况持续下去,我将编写一个基于最快可能的委托的 delegate,看看性能问题是否会消失。

我大多数时候只使用函数指针用于遗留 API,或者用于创建用于在不同编译器生成的代码之间进行通信的 C 接口。当我实现跳转表、类型擦除等等的时候,我也把它们作为内部实现的细节: 当我生成和使用它的时候,而不是在外部将它公开给任何客户端代码使用,函数指针做我需要的所有事情。

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

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

其他的答案基于技术上的优点。我会给你一个基于经验的答案。

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

但是我发现,结合了 lambdas 之类的功能,我的工作得到了相当大的释放,可以随心所欲地加入多个参数,重新排序它们,忽略调用者想要提供但我不需要的参数,等等。它确实使开发感觉更松散,响应更快,节省了我的时间,并增加了清晰度。

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

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