为什么 std::function 中的位置不变性很重要?

4

我一直在查看GCC实现的std::function(在调试时进入并偏离了主题)。

据我所见,它会将小类型存储在本地存储器中,而任何不适合本地存储器的内容都会通过新操作符分配。但是构造函数也会检查__location_invariant元函数,它是std::trivially_copyable特性的包装器,如果它不是“位置不变量”,还会在堆上进行分配。

我不完全明白为什么要这样做,因为据我所知,::new(storage)T(args)应该与new T(args)提供相同的结果。唯一的例外是就地构造函数不会分配任何内存。

如果它,例如,使用单个引用计数对象来存储太大而无法适应本地存储器的“位置不变量”类型,那么这将减少分配和复制的数量。对于非不变量对象,每次都要分配和复制,因为它们是“位置相关”的,不能全部引用同一存储区域。

实现似乎只对任何不适合或不是位置不变量的内容进行堆分配(至少我没有看到它这样做?),所以我很困惑它为什么需要检查位置不变性,如果在功能上没有明显的区别。

1个回答

6

看起来libstdc++使用“位置不变”属性来简化对std :: function 的某些操作。即,可调用对象的存储由一个名为{{link1:union named _Any_data}}的联合体提供,其中包含一个char数组。这个char数组可以为指向实际可调用对象的指针(如果它在堆上分配)或可调用对象本身(如果它符合小对象优化的条件)提供存储。当移动构造std :: function 时,RHS的 _Any_data 成员只需要被轻松地复制到* this _Any_data 成员中(加上必须以某种方式指示RHS为空)。这适用于 _Any_data 存储指向堆分配的可调用对象的指针(因为指针是可以轻松复制的),也适用于存储小型可调用对象的情况(因为在这种情况下,可调用对象要求是可以轻松复制的)。同样, std :: function 上的交换操作可以实现为 _Any_data 成员的平凡交换,而复制/移动赋值操作都使用复制交换惯用语实现。

有可能更加慷慨一些:小对象优化理论上适用于任何可调用类型,只要该类型具有nothrow-copy-constructible或nothrow-move-constructible。[1] 但是,在类型不是平凡可复制的情况下,这会给实现带来额外的复杂性。以std::function的移动构造函数为例,如果RHS可能内联存储一个非平凡可复制的对象,则必须有条件地调用此类可调用的复制构造函数,具体取决于存储的元数据是否指示该构造函数为非平凡构造函数。这不难实现:只需将额外的方法添加到管理器对象中即可。但是,它意味着每次移动构造std::function时都必须执行额外的间接函数调用。在交换操作的情况下,需要进行3次这样的调用。

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

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

谢谢解释!非常有启发性! - JustClaire

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接