shrink_to_fit 的 libstdc++ 和 libc++ 实现看起来都符合我的要求。但它们非常不同,主要是因为这两种实现遵循不同的类不变量。
首先,对于那些不知道的人,std::deque 是一个指向固定大小的T 数组的指针数组(通常是T*,但自定义分配器可以对其进行泛化)。指针数组可以被视为一个循环缓冲区,或者可以将其数据的开头从缓冲区的开头滑开的缓冲区。
这个答案将只关注双端队列的固定长度数组。
libstdc++
libstdc++ 实现有一个不变量,即deque 中永远不会有空数组。如果pop_front 或pop_back 创建了一个空数组,则该数组在擦除期间被释放。这种设计减少了deque::shrink_to_fit 实际可以实现的功能,因为deque 始终处于或非常接近最小内存占用量。
libstdc++ shrink_to_fit 仅当它可以向自己证明它可以通过这样做来消除数组时才执行复制和交换。例如,deque 可以保存由三个固定长度数组支持的 1010 个值,每个数组的容量为 1000 个值。 deque 中的第一个元素可能从第一个数组中的位置 995 开始。因此,第一个和第三个数组中的大多数(但不是全部)都是空的。复制/交换将分配两个新数组,将 1010 个元素复制/移动到这两个数组中,然后释放旧的 3 个数组。
libc++
libc++ 实现遵循稍微不同的设计,旨在加速FIFO queues。当pop_front(或pop_back)清空一个数组时,除非前面(或后面)已经有一个空数组,否则该数组不会被释放。 IE。虽然 libstdc++ 不变量是在整个 deque 中永远不会有一个空数组,但 libc++ 不变量是在 deque 的前后可以有零个或一个空数组。这样做的基本原理是,如果push_back(或push_front)需要一个新数组,它首先在deque 的另一端寻找一个空数组并从那里加固,然后再分配一个新数组。给定一个大小近似恒定的 FIFO 队列,此设计将达到这样一种状态:pop_front/push_back 序列永远不会分配新数组。相反,数组从 deque 的前面循环到后面(反之亦然)。
libc++ shrink_to_fit 将丢弃 deque 两端的空数组(如果存在)。相反,libstdc++ 不需要这样做,因为空数组从不存在。 libc++ shrink_to_fit 不会尝试通过复制/交换操作将值“移动”到更接近块开头的位置来进一步减少内存占用。
结果是 libc++ shrink_to_fit 永远不会使引用无效,而 libstdc++ shrink_to_fit 通常会。请注意,shrink_to_fit 的规范旨在允许引用无效,尽管现在我看,我认为它错误地没有(它必须用于 vector::shrink_to_fit,并且deque 的措辞取自vector 的措辞)。
这两个实现也将shrink_to_fitT* 的底层缓冲区,尽管这具有相对较小的影响,因为T* 的缓冲区的内存占用量通常远小于数组的内存占用量。每个 libc++ 数组通常占用 4096 字节(1 页),而每个 libstdc++ 数组通常占用 512 字节。
我不知道 VC++ deque 做了什么。但它也会实现一个数组数组。
正如所见,虽然所有实现都在相同的数据结构上运行(为了符合复杂性和失效要求),但仍有一些重要的设计决策留给实现,每个实现都力求做到提供其认为最适合客户的产品。