为什么位置不变性对于 std::function 很重要?

Jus*_*ire 4 c++

我一直在研究 GCC 的实现std::function(在调试时到达那里,然后偏离了主题)。

据我所知,它在本地存储中存储小型类型,并且任何不适合它的内容都通过 new 运算符分配。然而,构造函数也会检查元函数__location_invariant,它是特征的包装器std::trivially_copyable,如果它不是“位置不变”,它也会在堆上分配它。

我不完全理解它为什么这样做,因为据我了解 ::new (storage) T(args) 应该提供与就地构造函数相同的结果 new T(args) ,但就地构造函数不分配任何内存。

例如,如果它使用单个引用计数对象来存储太大而无法放入本地存储的“位置不变”类型,那么对我来说会更有意义,因为这会减少分配和复制的数量。由于非不变对象每次都被分配和复制,因此它们是“位置相关的”,它们不能全部引用相同的存储。

该实现似乎只是堆分配任何不适合和/或不是位置不变的东西(至少我没有看到它这样做?),所以我很困惑为什么它需要检查位置不变性,如果功能上没有明显差异。

Bri*_*ian 6

看来 libstdc++ 使用“位置不变”属性来简化std::function. 即,可调用对象的存储由包含 char 数组的命名联合提供。_Any_data此 char 数组可以为指向实际可调用对象的指针提供存储(如果它在堆上分配),也可以为可调用对象本身提供存储(如果它符合小对象优化的条件)。当std::function移动构造时,_Any_dataRHS 的成员只需要简单地复制到 的_Any_data成员*this(加上 RHS 必须以某种方式指示为空)。当_Any_data存储指向堆分配的可调用对象的指针时(因为该指针是可简单复制的)以及存储小型可调用内联对象时(因为在这种情况下,可调用对象需要可简单复制),这都有效。类似地,交换操作std::function可以实现为_Any_data成员的简单交换,并且复制/移动分配操作都使用复制交换惯用法来实现。

可以更慷慨一些:理论上,任何不可抛出复制构造或不可抛出移动构造的可调用类型都可以支持小对象优化。[1] 然而,在类型不可简单复制的情况下,这会给实现带来额外的复杂性。考虑如何编写移动构造函数,std::function以防 RHS可能内联存储不可简单复制的对象。在这种情况下,必须有条件地调用此类可调用对象的复制构造函数,具体取决于存储的元数据是否表明此类构造函数不平凡。这并不难实现:只需将附加方法添加到管理器对象即可。然而,这意味着每次std::function移动构造 a 时都必须执行额外的间接函数调用。如果是交换操作,则需要 3 次这样的调用。

实现者需要做出的权衡是,适合小对象缓冲区且不可抛出移动(但不可简单复制)的类型是否足够常见,以至于允许它们存储在小对象缓冲区中的好处是否超过了所有可调用类型的移动和交换操作使用的额外间接函数调用的成本。

[1] 这一要求之所以必要的原因(或至少一个原因)是交换两个std::function对象需要始终成功。交换操作实际上必须重新定位内联存储的任何值(而不是堆上的值,在这种情况下,指针的所有权可以简单地转移到另一个std::function对象)。如果此类重定位涉及的基础副本或移动不是noexcept,则无法保证交换会成功;因此堆分配是唯一的选择。