std :: function的替代品,用于收集callables

时间:2016-12-18 01:09:45

标签: c++ c++11 templates c++14

除了诉诸std::function之外,还有其他方法可以存储同类的可调用集合吗?即,在以下代码中替换类型T

using T = std::function<void(int)>;
std::vector<T> v{some_lambda, some_fn_ptr, some_pmf, some_functor};

还有其他什么?

当将单个callables作为参数传递给高阶函数时,我尽可能使用模板来避免std::function的开销。但是对于收藏品,我不知道我能做些什么。

3 个答案:

答案 0 :(得分:4)

直接类型减少开销的最大来源是内联函数的能力。在重复应用的紧密循环中,内联函数有时可以被矢量化或以其他方式获得大量优化。

来自std::function的第二个开销来源是它使用虚函数表。这导致两个随机访问&#34;内存查找 - 一个用于vtable,另一个用于跟踪vtable上的指针。如果这些不在以前使用的缓存中,则相当昂贵。但是,如果它们位于缓存中,则最终并不昂贵(一些指令)。

std::function的最后一笔开销来源是内存分配。如果存储在std::function中的对象很大(在MSVC中作为示例,大于sizeof(std::string)*2,其中std::string本身使用SBO,因此其大小适中),则会发生堆分配。因此,无论何时复制或创建std::function,都会有相当高的成本。

这些都可以减轻。

自定义std::function克隆可以使用无vtable的调用类型擦除来降低#2的成本。这有大小成本。

可以写入不存储可调用对象的function_ref类型。这些存储void*和等效的vtable(或指向方法的直接指针)。或者,可以编写具有自定义存储大小和拒绝堆分配的std::function克隆。要么以合理的灵活性和/或缺乏价值语义来合理地缓解#3。

第一个是最难减轻的。

如果您知道将使用您的callable执行哪些操作,而不是删除到泛型调用,您可以在上下文中删除调用。

例如,假设您具有逐像素操作。在图像的每个像素上调用std::function将会产生大量开销。

然而,如果不是擦除每个像素的调用(或者同样),我们擦除每个像素运行的调用,我们现在可以一般地存储我们的可调用,并且开销从每像素到每个扫描线或每个图像!

现在可以在紧密循环中看到callable,因此编译器可以对其进行内联和向量化,并且每个扫描行只执行一次后续工作。

你可以变得更加漂亮,甚至可以用线条擦除扫描线。或者有一些擦除,一个用于扫描线,零线路,一个用于倒置扫描线,另一个用于非零线路,等等。扫描线长度为2。

这些都有成本,而不是在开发时。如果您已经测试并确认std::function确实导致了问题,那么只能走这条路线。

答案 1 :(得分:3)

std::function的开销存在,因为它是值类型。它在内部存储您提供的仿函数的副本(它当然可以从中移动)。它确实键入了擦除,但由于它可以存储任意一侧的对象,因此它必须能够分配内存。

因此,如果您的需求不需要该函数实际存储对象,如果它只能引用一个将继续存在的对象,那么您会没事的。您可以找到几种类型的实现,通常称为function_ref。这种类型从不分配内存;虽然他们的开销不是零,但它没有function那么大。

但是,这些是引用到现有函数。因此,对于仿函数,它们引用了一个真正的C ++对象(而不是函数/成员指针)。这造成了潜在的终身问题。对于立即回调(您将它们传递给仅在其持续时间内调用该回调的函数),它不是一个问题。但在你的情况下,它可能不会有用。

最终,如果您无法解决生命周期问题,那么使用std::function的开销实际上对您有用。所以最好接受它。

答案 2 :(得分:0)

仍然允许您在此处指定的所有替代方案

 std::vector<T> v{some_lambda, some_fn_ptr, some_pmf, some_functor};

你一定可以创建自己的模板声明来捕捉所有这些声明。虽然我无法看到它比std::function更有效或更好,除非您要对该类型做出更多限制。