std::function 内部存储组织和副本;传递参考与价值

std::function internal memory organization and copies; passing reference vs value

复制std::function时,它引用的代码指令是否也被复制?

std::function 是通过某种形式的可调用对象初始化的,它以某种方式指向可执行代码(就像函数指针通常那样)。现在,当一个函数对象被复制时,这个可执行代码运行时是被复制还是在内部被引用? 换句话说这个问题:如果 std::function 的一个实例被复制,那么内存中是否有相同编译代码指令的多个副本? std::function 是实际存储函数代码的对象还是函数指针的抽象?

前者看起来很浪费,我不怀疑,但到目前为止我发现的关于这个主题的所有内容要么太模糊、缺乏,要么太具体,我无法确定。例如

When the target is a function pointer or a std::reference_wrapper, small object optimization is guaranteed, that is, these targets are always directly stored inside the std::function object, no dynamic allocation takes place. Other large objects may be constructed in dynamic allocated storage and accessed by the std::function object through a pointer. - cppreference

给出了一些关于它是如何完成的提示,但似乎仍然太模糊,并且可能与这个问题根本无关,因为 std::function.

内部的进一步抽象

对于上下文:我正在尝试重构一些糟糕的 C-ish 代码,这些代码将输入事件(击键、鼠标输入等)映射到特定行为,即在目标数据结构上执行,程序可以将其解释为具有语义上下文而不是击键(又名键绑定)的更具体的输入。人们可以怀疑行为要求差异很大。
这以前是通过指定输入事件 ID 的定义和数字列表以及由 switch-case 选择的硬编码行为实现的。我们很快就接近了这种最初的做法变得笨拙的边界。
为了摆脱定义列表的可扩展性、声明性、面向对象和灵活的设计,我考虑了高阶函数。
特别是因为某些行为非常简单并且反复需要(例如输出数据结构中一个值的切换)其他行为在附加多个条件的情况下更加复杂,我想静态地声明一些行为,但仍然会喜欢在某些情况下只分配一些特殊的 lambda。由于我需要为每个输入(键、鼠标按钮、鼠标轴等)存储行为,并且可能一次可以为不同的键绑定集实例化一种特定行为类型的许多副本,我想知道是否应该引用这种行为,而不是按值存储。在前一种情况下,新的 lambda 需要由行为结构拥有,但静态声明的行为不需要,这在实用上会导致一些 shared_ptr 恶作剧。在后一种情况下,按价值来说,这不是问题,但我不希望例如切换行为的多个副本导致过多的冗余开销。

我认为关于例外情况的信息有一些启示:

Does not throw if other's target is a function pointer or a std::reference_wrapper, otherwise may throw std::bad_alloc or any exception thrown by the constructor used to copy or move the stored callable object. CppReference

这似乎意味着 std::function 的每个副本也复制包含的可调用对象。例如,如果您的函数包含带有向量的 lambda,则复制 lambda 和结果向量。链接到它的实际机器代码保留在可执行文件的 read-only 部分,不会被复制。

来自 the c++20 standard draft 的更新:20.14.16.2.1 构造函数和析构函数[func.wrap.func.con]

function(const function& f);

Postconditions: !*this if !f; otherwise, *this targets a copy off.target().

Throws: Nothing iff’s target is a specialization ofreference_wrapperor a function pointer. Otherwise, may throwbad_allocor any exception thrown by the copy constructor of the stored callable object.

[Note: Implementations should avoid the use of dynamically allocated memory for small callable objects for example, where f’s target is an object holding only a pointer or reference to an object and a member function pointer. — end note]

std::function 似乎只管理一个可调用对象。 如果复制,代码会发生什么由可调用本身指定。

在函数指针的情况下,只需要复制一个函数指针。

在 lambda 或自定义可调用的情况下,这将由 lambda 或任何自定义可调用副本的实现决定 class。 后两者通常可以在代码引用之外拥有自己的成员。因此,一些 space 必须由 std::function 分配以适应这些情况。然而,这是误导性的,因为它似乎 std::function 为代码分配 space。指令代码的管理似乎是由可调用对象完成的,但是这是在内部完成的。

在这种情况下,复制时通常使用的可调用对象(如 lambda)的默认行为对于预期的问题似乎更有趣,但似乎确实将提出的问题延伸到 [=10 的上下文范围之外=].

因此,我会认为这个问题已解决,并加深了我对如何实现 lamda 的了解,尤其是关于它们的编译方式和引用的编译代码。

(注意:下面的整个讨论有点简化。据我所知,其中 none 是错误的,但我确实省略了一些细节和边缘情况以及定义和实现的东西。)

std::function不会复制任何可执行代码。可执行代码总是仅由 std::function 指向。当 std::function 被复制时,指针被复制(这完全没问题,因为可执行代码也从未被释放。)到目前为止,普通的旧函数指针和 std::function 之间没有区别.

但这还不是全部。

与函数指针相反,std::function 的实例可以携带“状态”以及指向可执行代码的指针,以及关于 std::function 必须 allocate/deallocate 的整个喧嚣copy/move 周围的数据是关于这个额外状态的 ,而不是函数指针

假设您有这样的代码:

(请注意,虽然我在这里使用了 lambda,但以下解释同样适用于“函子”和“函数对象”和“绑定结果”以及 C++ 中其他形式的可调用对象,所有除了普通的旧函数指针。)

int x = 42, y = 17;
std::function<int()> f = [x, y] {return x + y;};

这里,f不仅要存储return x + y;的可执行代码的指针,还要记住xy的值。由于您可以通过这种方式“捕获”的状态量不受限制,因此 - 根据定义 - std::function 必须在构造时从堆中分配内存,并在适当的时候释放它,复制它并移动它.同样,被复制的是这个额外的“状态”,而不是代码。

让我们回顾一下:每个 std::function 需要能够存储至少一个指向可执行代码的指针,以及 0 个或更多字节的额外捕获状态。如果没有捕获状态,std::function 本质上与函数指针相同(尽管在实践中,std::function 通常以多态方式实现,并且其中包含其他内容。)

据我所知,std::function 的一些(大多数)实现采用了一种称为“小对象优化”的优化。在这些实现中,除了指向代码的指针 space 之外,std::function 对象在其实例中还有一些(固定数量的)space(即作为其 class,而不是堆上的其他地方),如果捕获状态的总字节数适合那里,将使用该区域。这消除了堆分配,这在某些用例中很重要,并且会平衡使用的额外内存(当没有或只有很少的状态要捕获时。)