【问题标题】:std::function internal memory organization and copies; passing reference vs valuestd::function 内部存储器组织和副本;传递参考与价值
【发布时间】:2020-09-12 19:24:08
【问题描述】:

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

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

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

当目标是函数指针或 std::reference_wrapper 时,保证小对象优化,即这些目标总是直接存储在 std::function 对象中,不会发生动态分配。其他大对象可以在动态分配的存储中构造并由 std::function 对象通过指针访问。 - cppreference

给出了一些关于它是如何完成的提示,但似乎仍然太模糊,并且可能与这个问题完全无关,因为std::function 内部有进一步的抽象。

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

【问题讨论】:

  • std::function 是一个对象,就像任何 c++ 对象一样。复制向量时是否担心复制代码?
  • 向量通常在缓冲区中没有可调用的指令,这些指令已被编译。
  • 许多执行环境不支持动态代码生成(例如 ROM、许多手机应用商店)。即使那些支持它的人通常也不会提供足够的信息来复制和重新定位任意代码。 C++ 依赖编译器在编译时生成代码。实例数据当然是经常复制的。
  • “它引用的代码指令”是什么意思?可执行文件是在编译时生成的。在运行时,只有值被操纵和传递。我觉得这个基本概念中的某些东西在这里被遗漏了。

标签: c++ std-function


【解决方案1】:

(注意:下面的整个讨论有点简化。AFAIK,没有一个是错的,但我确实省略了一些细节和边缘案例以及定义和实现的东西。)

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

但这还不是全部。

与函数指针相反,std::function 的实例可以携带“状态”以及指向可执行代码的指针,而关于 std::function 必须分配/解除分配和复制/移动数据的整个喧嚣是 关于这个额外的状态,而不是函数指针。

假设你有这样的代码:

(请注意,虽然我在这里使用了 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::functions 通常以多态方式实现,并且其中包含其他内容。)

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

【讨论】:

  • 就像我写的那样,我没有怀疑指令代码被复制,但围绕主题的信息很棘手。你的解释比我自己的回答更能解决困惑,所以我会接受这个。
  • 对于 Visual C++,反汇编显示,捕获存储在堆栈中,并且调用了 lambda 的相同代码地址。即使没有捕获,也不会使用内联扩展。
【解决方案2】:

我认为关于例外的信息有一些共同点:

如果 other 的目标是函数指针或 std::reference_wrapper 则不抛出,否则可能抛出 std::bad_alloc 或用于复制或移动存储的可调用对象的构造函数抛出的任何异常。 CppReference

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

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

函数(const function& f);

后置条件:!*this if !f;否则,*this 以副本 off.target() 为目标。

抛出:什么都没有当且仅当的目标是reference_wrapperor的特化 一个函数指针。否则,可能会抛出bad_allocor任何异常 由存储的可调用对象的复制构造函数抛出。

[注: 实现应该避免使用动态分配的内存 例如对于小的可调用对象,其中 f 的目标是一个对象,它只包含一个指针或对一个对象的引用和一个成员函数指针。 ——尾注]

【讨论】:

  • 这意味着如果 std::function 管理一个可调用对象,并且它是一个引用或函数指针,那么它不会管理资源本身,但在所有其他情况下它会管理。
  • 那句话出自哪里?听起来不对。
  • 即使对于reference_wrapper,它也管理reference_wrapper 类,不过,这是对实际可调用对象的浅层引用。而对于函数指针,它也会将地址复制到函数中
  • @PeteBecker CppReference,问题引用文本下方几行
  • 虽然这肯定指向问题的答案,但我认为它并不明确。此外,它似乎通过复制管理可调用的“目标”(如果复制自身),然后有关指令代码的实际行为将由该可调用指定。也许我应该在这方面更新问题。
【解决方案3】:

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

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

在 lambda 或自定义可调用情况下,这将由 lambda 副本或任何自定义可调用类的实现来确定。 后两者通常可以在对代码的引用之外拥有自己的成员。因此std::function 必须分配一些空间来容纳这些情况。然而,这具有误导性,因为它可能看起来 std::function 为代码分配空间。指令代码的管理似乎是由 callable 完成的,但这是在内部完成的。

在这种情况下,通常使用的可调用对象(如 lambdas)在复制时的默认行为似乎对预期问题更有趣,但似乎确实将提出的问题超出了std::function 的上下文范围。

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

【讨论】:

  • 即使使用 lambda,也不会复制任何代码;仅复制捕获。一般来说,在标准 C++ 中,无法操作/复制实际指令,因此复制可调用对象不应复制指令(除非可调用对象执行某些 JITting)。
猜你喜欢
  • 2015-02-06
  • 1970-01-01
  • 2011-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-01
  • 2021-07-12
相关资源
最近更新 更多