| // Copyright (c) 2026 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef QUICHE_COMMON_STABLE_BLOCK_LIST_H_ |
| #define QUICHE_COMMON_STABLE_BLOCK_LIST_H_ |
| |
| #include <algorithm> |
| #include <array> |
| #include <bit> |
| #include <cstddef> |
| #include <cstdint> |
| #include <iterator> |
| #include <memory> |
| #include <type_traits> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/macros.h" |
| #include "absl/base/nullability.h" |
| #include "quiche/common/platform/api/quiche_export.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| |
| namespace quiche { |
| |
| // StableBlockList: A block-based append-only sequence container with iterator |
| // stability. |
| // |
| // StableBlockList is designed for use cases where elements are only appended to |
| // the end or erased from arbitrary positions, such as the backing store of a |
| // LinkedHashMap. |
| // |
| // It provides 100% iterator stability on push_back/emplace_back and erase. |
| // Iterators to erased elements are invalidated, but all other iterators remain |
| // valid. |
| // |
| // Internally, it manages a doubly-linked list of fixed-capacity blocks. |
| template <typename T, size_t BlockCapacity = 16, // QUICHE_NO_EXPORT |
| typename Allocator = std::allocator<T>> // QUICHE_NO_EXPORT |
| class StableBlockList { // QUICHE_NO_EXPORT |
| static_assert(BlockCapacity > 0, "BlockCapacity must be greater than 0"); |
| static_assert(BlockCapacity <= 64, "BlockCapacity cannot exceed 64"); |
| |
| private: |
| // Selects the smallest unsigned integer type that can hold BlockCapacity |
| // bits. This minimizes block metadata overhead (2B for capacity<=16, 4B for |
| // capacity<=32, 8B for capacity<=64). |
| using OccupancyType = std::conditional_t< |
| BlockCapacity <= 16, uint16_t, |
| std::conditional_t<BlockCapacity <= 32, uint32_t, uint64_t>>; |
| |
| static constexpr int kBitWidth = sizeof(OccupancyType) * 8; |
| |
| // --- Bitwise Math & Manipulation Helpers --- |
| // These map to single-cycle CPU instructions (e.g. tzcnt, lzcnt, bsf, bsr) |
| // for O(1) operations. |
| |
| // Finds the index of the lowest set bit. Returns default_idx if none. |
| static constexpr int LowestBitIndex(OccupancyType bits, int default_idx) { |
| return bits != 0 ? std::countr_zero(bits) : default_idx; |
| } |
| |
| // Finds the index of the highest set bit. Returns default_idx if none. |
| static constexpr int HighestBitIndex(OccupancyType bits, int default_idx) { |
| return bits != 0 ? (kBitWidth - 1 - std::countl_zero(bits)) : default_idx; |
| } |
| |
| // Clears (zeros out) all bits at indices <= start_idx. |
| [[nodiscard]] static constexpr OccupancyType ClearLowBits(OccupancyType bits, |
| int start_idx) { |
| QUICHE_DCHECK_GE(start_idx, 0); |
| QUICHE_DCHECK_LE(start_idx, kBitWidth); |
| if constexpr (kBitWidth < 64) { |
| // Safe to shift without guard because start_idx <= kBitWidth <= 32. |
| uint64_t mask = ~((uint64_t{1} << start_idx) - 1); |
| return static_cast<OccupancyType>(static_cast<uint64_t>(bits) & mask); |
| } else { |
| if (start_idx >= 64) return 0; |
| uint64_t mask = ~((uint64_t{1} << start_idx) - 1); |
| return static_cast<OccupancyType>(bits & mask); |
| } |
| } |
| |
| // Clears (zeros out) all bits at indices >= end_idx. |
| [[nodiscard]] static constexpr OccupancyType ClearHighBits(OccupancyType bits, |
| int end_idx) { |
| QUICHE_DCHECK_GE(end_idx, 0); |
| QUICHE_DCHECK_LE(end_idx, kBitWidth); |
| if (end_idx <= 0) return 0; |
| if constexpr (kBitWidth < 64) { |
| // Safe to shift without guard because end_idx <= kBitWidth <= 32. |
| uint64_t mask = (uint64_t{1} << end_idx) - 1; |
| return static_cast<OccupancyType>(static_cast<uint64_t>(bits) & mask); |
| } else { |
| if (end_idx >= 64) return bits; |
| uint64_t mask = (uint64_t{1} << end_idx) - 1; |
| return static_cast<OccupancyType>(bits & mask); |
| } |
| } |
| |
| static constexpr bool IsBitSet(OccupancyType bits, int index) { |
| return (bits & (OccupancyType{1} << index)) != 0; |
| } |
| |
| [[nodiscard]] static constexpr OccupancyType SetBit(OccupancyType bits, |
| int index) { |
| return bits | (OccupancyType{1} << index); |
| } |
| |
| [[nodiscard]] static constexpr OccupancyType ClearBit(OccupancyType bits, |
| int index) { |
| return bits & ~(OccupancyType{1} << index); |
| } |
| |
| struct Block; |
| |
| // Lightweight header for blocks to allow sentinel node in ControlBlock |
| // without wasting memory for elements. |
| struct BlockHeader { |
| BlockHeader* prev = this; // Not used when a block is in the freelist. |
| BlockHeader* next = this; |
| OccupancyType occupancy = 0; |
| }; |
| |
| // Fixed-capacity node (block) holding contiguous elements. |
| struct Block : public BlockHeader { |
| alignas(T) char data[BlockCapacity * |
| sizeof(T)]; // Uninitialized storage for elements |
| |
| T* ElementAt(size_t index) { return reinterpret_cast<T*>(data) + index; } |
| const T* ElementAt(size_t index) const { |
| return reinterpret_cast<const T*>(data) + index; |
| } |
| }; |
| |
| using BlockAllocator = |
| typename std::allocator_traits<Allocator>::template rebind_alloc<Block>; |
| |
| // ControlBlock holds metadata and sentinel. It is heap-allocated and |
| // owned via unique_ptr to keep block pointers and iterators stable when the |
| // list is moved. |
| struct ControlBlock { |
| BlockHeader sentinel; // next/prev point to the head/tail |
| size_t size = 0; // Total number of elements in the list |
| Block* free_blocks = nullptr; // Singly-linked LIFO pool of recycled blocks |
| }; |
| |
| public: |
| template <bool is_const> |
| class IteratorImpl { |
| public: |
| using iterator_category = std::bidirectional_iterator_tag; |
| using value_type = T; |
| using difference_type = std::ptrdiff_t; |
| using pointer = typename std::conditional<is_const, const T*, T*>::type; |
| using reference = typename std::conditional<is_const, const T&, T&>::type; |
| |
| IteratorImpl() = default; |
| |
| // Iterators reference elements by their block pointer and offset. |
| IteratorImpl(BlockHeader* block, uint32_t block_index) |
| : block_(block), block_index_(block_index) {} |
| |
| // Allows implicit conversion from iterator to const_iterator. |
| template <bool was_const = is_const, |
| typename = typename std::enable_if<was_const>::type> |
| IteratorImpl(const IteratorImpl<false>& other) |
| : block_(other.block_), block_index_(other.block_index_) {} |
| |
| reference operator*() const { |
| ABSL_HARDENING_ASSERT(block_ != nullptr); |
| ABSL_HARDENING_ASSERT(block_->occupancy != 0 && |
| "Attempted to dereference end()"); |
| return *static_cast<pointer_to_block>(block_)->ElementAt(block_index_); |
| } |
| pointer operator->() const { |
| ABSL_HARDENING_ASSERT(block_ != nullptr); |
| ABSL_HARDENING_ASSERT(block_->occupancy != 0 && |
| "Attempted to dereference end()"); |
| return static_cast<pointer_to_block>(block_)->ElementAt(block_index_); |
| } |
| |
| IteratorImpl& operator++() { |
| *this = Next(*this); |
| return *this; |
| } |
| |
| IteratorImpl operator++(int) { |
| IteratorImpl tmp = *this; |
| ++*this; |
| return tmp; |
| } |
| |
| IteratorImpl& operator--() { |
| *this = Prev(*this); |
| return *this; |
| } |
| |
| IteratorImpl operator--(int) { |
| IteratorImpl tmp = *this; |
| --*this; |
| return tmp; |
| } |
| |
| bool operator==(const IteratorImpl& other) const = default; |
| |
| private: |
| using pointer_to_block = |
| typename std::conditional<is_const, const Block*, Block*>::type; |
| |
| BlockHeader* block_ = nullptr; |
| uint32_t block_index_ = 0; |
| |
| friend class StableBlockList; |
| template <bool> |
| friend class IteratorImpl; |
| }; |
| |
| using value_type = T; |
| using size_type = size_t; |
| using difference_type = std::ptrdiff_t; |
| using reference = T&; |
| using const_reference = const T&; |
| using pointer = T*; |
| using const_pointer = const T*; |
| using iterator = IteratorImpl<false>; |
| using const_iterator = IteratorImpl<true>; |
| using reverse_iterator = std::reverse_iterator<iterator>; |
| using const_reverse_iterator = std::reverse_iterator<const_iterator>; |
| |
| // Copies are not allowed. |
| StableBlockList(const StableBlockList&) = delete; |
| StableBlockList& operator=(const StableBlockList&) = delete; |
| |
| // Moves are allowed. |
| StableBlockList(StableBlockList&& other) noexcept |
| : allocator_(std::move(other.allocator_)), |
| control_block_(std::move(other.control_block_)) {} |
| |
| StableBlockList& operator=(StableBlockList&& other) noexcept { |
| if (this != &other) { |
| this->~StableBlockList(); |
| new (this) StableBlockList(std::move(other)); |
| } |
| return *this; |
| } |
| |
| explicit StableBlockList(const Allocator& alloc = Allocator()) |
| : allocator_(alloc), control_block_(std::make_unique<ControlBlock>()) {} |
| |
| ~StableBlockList() { |
| if (!control_block_) return; |
| clear(); |
| shrink_to_fit(); |
| } |
| |
| bool empty() const { |
| return control_block_ ? (control_block_->size == 0) : true; |
| } |
| size_t size() const { return control_block_ ? control_block_->size : 0; } |
| |
| // Standard element accessors. |
| T& front() { |
| return const_cast<T&>(const_cast<const StableBlockList*>(this)->front()); |
| } |
| const T& front() const { |
| QUICHE_DCHECK(!empty()); |
| return *begin(); |
| } |
| |
| T& back() { |
| return const_cast<T&>(const_cast<const StableBlockList*>(this)->back()); |
| } |
| const T& back() const { |
| QUICHE_DCHECK(!empty()); |
| return *rbegin(); |
| } |
| |
| // Standard iterator accessors. |
| iterator begin() { |
| if (empty()) { |
| return end(); |
| } |
| BlockHeader* head = control_block_->sentinel.next; |
| return iterator(head, LowestBitIndex(head->occupancy, BlockCapacity)); |
| } |
| const_iterator begin() const { |
| return const_cast<StableBlockList*>(this)->begin(); |
| } |
| const_iterator cbegin() const { return begin(); } |
| |
| iterator end() { |
| return iterator(control_block_ ? &control_block_->sentinel : nullptr, 0); |
| } |
| const_iterator end() const { |
| return const_iterator( |
| control_block_ ? const_cast<BlockHeader*>(&control_block_->sentinel) |
| : nullptr, |
| 0); |
| } |
| const_iterator cend() const { return end(); } |
| |
| reverse_iterator rbegin() { return reverse_iterator(end()); } |
| const_reverse_iterator rbegin() const { |
| return const_reverse_iterator(end()); |
| } |
| const_reverse_iterator crbegin() const { |
| return const_reverse_iterator(end()); |
| } |
| |
| reverse_iterator rend() { return reverse_iterator(begin()); } |
| const_reverse_iterator rend() const { |
| return const_reverse_iterator(begin()); |
| } |
| const_reverse_iterator crend() const { |
| return const_reverse_iterator(begin()); |
| } |
| |
| // Elements may be appended. |
| void push_back(const T& value) { InsertToEnd(value); } |
| void push_back(T&& value) { InsertToEnd(std::move(value)); } |
| |
| // TODO(birenroy): Remove these restricted insert/emplace methods once |
| // QuicheLinkedHashMap is updated to use push_back/emplace_back. |
| iterator insert(const_iterator pos, const T& value) { |
| QUICHE_DCHECK(pos == end()) |
| << "StableBlockList only supports insertion at the end"; |
| return InsertToEnd(value); |
| } |
| iterator insert(const_iterator pos, T&& value) { |
| QUICHE_DCHECK(pos == end()) |
| << "StableBlockList only supports insertion at the end"; |
| return InsertToEnd(std::move(value)); |
| } |
| |
| template <typename... Args> |
| iterator emplace(const_iterator pos, Args&&... args) { |
| QUICHE_DCHECK(pos == end()) |
| << "StableBlockList only supports emplace at the end"; |
| return InsertToEnd(std::forward<Args>(args)...); |
| } |
| |
| // Swaps are efficient, and do not require any copies. |
| void swap(StableBlockList& other) noexcept { |
| using std::swap; |
| swap(control_block_, other.control_block_); |
| if constexpr (std::allocator_traits< |
| Allocator>::propagate_on_container_swap::value) { |
| swap(allocator_, other.allocator_); |
| } |
| } |
| |
| // Elements may be erased at any position. |
| iterator erase(const_iterator pos) { |
| BlockHeader* header = pos.block_; |
| QUICHE_DCHECK(header != &control_block_->sentinel); |
| Block* block = static_cast<Block*>(header); |
| |
| // Capture the next element before destroying the current one |
| iterator next_it = Next(iterator(header, pos.block_index_)); |
| |
| // Destroy the element and clear its occupancy bit |
| std::allocator_traits<Allocator>::destroy( |
| allocator_, block->ElementAt(pos.block_index_)); |
| block->occupancy = ClearBit(block->occupancy, pos.block_index_); |
| --control_block_->size; |
| |
| // If the block becomes completely empty, unlink and recycle it |
| if (block->occupancy == 0) { |
| BlockHeader* prev = block->prev; |
| BlockHeader* next = block->next; |
| |
| prev->next = next; |
| next->prev = prev; |
| |
| DeallocateBlock(block); |
| } |
| |
| // If we erased the last element, next_it will be end() which points to |
| // sentinel. |
| return next_it; |
| } |
| |
| iterator erase(const_iterator first, const_iterator last) { |
| while (first != last && first != end()) { |
| first = erase(first); |
| } |
| return iterator(first.block_, first.block_index_); |
| } |
| |
| // The clear operation removes all elements from the list, but does not free |
| // the memory of any allocated blocks. These are appended to the freelist. |
| void clear() { |
| if (!control_block_) return; |
| BlockHeader* curr = control_block_->sentinel.next; |
| while (curr != &control_block_->sentinel) { |
| Block* block = static_cast<Block*>(curr); |
| BlockHeader* next = curr->next; |
| for (size_t i = 0; i < BlockCapacity; ++i) { |
| if (IsBitSet(block->occupancy, i)) { |
| std::allocator_traits<Allocator>::destroy(allocator_, |
| block->ElementAt(i)); |
| } |
| } |
| DeallocateBlock(block); |
| curr = next; |
| } |
| control_block_->sentinel.next = &control_block_->sentinel; |
| control_block_->sentinel.prev = &control_block_->sentinel; |
| control_block_->size = 0; |
| // Note: we keep control_block_->free_blocks for reuse. |
| } |
| |
| // Releases all recycled blocks in the free list to free memory. |
| // |
| // Note: This method does not compact elements in active blocks or relocate |
| // them, as doing so would invalidate iterators and references, violating |
| // the iterator stability guarantee of StableBlockList. It only releases |
| // completely empty blocks that have been cached for reuse. |
| void shrink_to_fit() { |
| if (!control_block_) return; |
| BlockAllocator block_alloc(allocator_); |
| Block* curr = control_block_->free_blocks; |
| while (curr != nullptr) { |
| Block* next = static_cast<Block*>(curr->next); |
| std::allocator_traits<BlockAllocator>::deallocate(block_alloc, curr, 1); |
| curr = next; |
| } |
| control_block_->free_blocks = nullptr; |
| } |
| |
| private: |
| Block* AllocateBlock() { |
| BlockAllocator block_alloc(allocator_); |
| Block* block = nullptr; |
| if (control_block_->free_blocks != nullptr) { |
| block = control_block_->free_blocks; |
| control_block_->free_blocks = static_cast<Block*>(block->next); |
| } else { |
| block = std::allocator_traits<BlockAllocator>::allocate(block_alloc, 1); |
| } |
| std::allocator_traits<BlockAllocator>::construct(block_alloc, block); |
| return block; |
| } |
| |
| void DeallocateBlock(Block* block) { |
| BlockAllocator block_alloc(allocator_); |
| std::allocator_traits<BlockAllocator>::destroy(block_alloc, block); |
| |
| // Link to free blocks LIFO list using 'next' pointer |
| block->next = control_block_->free_blocks; |
| control_block_->free_blocks = block; |
| } |
| |
| template <typename It> |
| static It Next(It it) { |
| BlockHeader* curr = it.block_; |
| // Handles default-constructed iterators and end() of moved-from lists. |
| if (curr == nullptr) { |
| return it; |
| } |
| uint32_t i = it.block_index_; |
| |
| // Incrementing end() is undefined behavior. |
| QUICHE_DCHECK(curr->occupancy != 0) |
| << "Attempted to increment end() iterator"; |
| |
| // Find the next set bit in the current block's occupancy mask after index |
| // 'i' |
| OccupancyType bits = ClearLowBits(curr->occupancy, i + 1); |
| int next_idx = LowestBitIndex(bits, BlockCapacity); |
| if (next_idx < static_cast<int>(BlockCapacity)) { |
| return It(curr, next_idx); |
| } |
| |
| // If no more elements in this block, jump to the next block. |
| // Invariant: empty blocks are immediately unlinked in erase(), so any |
| // linked next block (if it's not the sentinel) is guaranteed to have |
| // occupancy > 0. |
| BlockHeader* next_b = curr->next; |
| if (next_b->occupancy != 0) { |
| int first_idx = LowestBitIndex(next_b->occupancy, BlockCapacity); |
| QUICHE_DCHECK_LT(first_idx, static_cast<int>(BlockCapacity)); |
| return It(next_b, first_idx); |
| } |
| |
| // Reached sentinel (end()) |
| return It(next_b, 0); |
| } |
| |
| template <typename It> |
| static It Prev(It it) { |
| BlockHeader* curr = it.block_; |
| if (curr == nullptr) { |
| return it; |
| } |
| uint32_t i = it.block_index_; |
| |
| // Find the previous set bit in the current block's occupancy mask before |
| // index 'i' |
| OccupancyType bits = ClearHighBits(curr->occupancy, i); |
| int prev_idx = HighestBitIndex(bits, -1); |
| if (prev_idx >= 0) { |
| return It(curr, prev_idx); |
| } |
| |
| // If no previous elements in this block, jump to the previous block. |
| BlockHeader* prev_b = curr->prev; |
| if (prev_b->occupancy != 0) { // If not sentinel |
| int last_idx = HighestBitIndex(prev_b->occupancy, -1); |
| QUICHE_DCHECK_GE(last_idx, 0); |
| return It(prev_b, last_idx); |
| } |
| |
| // Reached sentinel (begin() decrement goes to end()) |
| return It(prev_b, 0); |
| } |
| |
| template <typename... Args> |
| iterator InsertToEnd(Args&&... args) { |
| EnsureControlBlock(); |
| |
| BlockHeader* sentinel = &control_block_->sentinel; |
| BlockHeader* tail_header = sentinel->prev; |
| Block* tail = static_cast<Block*>(tail_header); |
| |
| int insert_idx = 0; |
| bool need_new_block = false; |
| |
| if (tail_header == sentinel) { |
| need_new_block = true; |
| } else { |
| int k = HighestBitIndex(tail->occupancy, -1); |
| if (k < static_cast<int>(BlockCapacity) - 1) { |
| insert_idx = k + 1; |
| } else { |
| need_new_block = true; |
| } |
| } |
| |
| if (need_new_block) { |
| Block* new_block = AllocateBlock(); |
| new_block->prev = tail_header; |
| new_block->next = sentinel; |
| tail_header->next = new_block; |
| sentinel->prev = new_block; |
| tail = new_block; |
| insert_idx = 0; |
| } |
| |
| std::allocator_traits<Allocator>::construct( |
| allocator_, tail->ElementAt(insert_idx), std::forward<Args>(args)...); |
| tail->occupancy = SetBit(tail->occupancy, insert_idx); |
| control_block_->size++; |
| return iterator(tail, insert_idx); |
| } |
| |
| void EnsureControlBlock() { |
| if (!control_block_) { |
| control_block_ = std::make_unique<ControlBlock>(); |
| } |
| } |
| |
| [[no_unique_address]] Allocator allocator_; |
| absl_nullable std::unique_ptr<ControlBlock> control_block_; |
| }; |
| |
| template <typename T, size_t BlockCapacity, typename Allocator> |
| void swap(StableBlockList<T, BlockCapacity, Allocator>& lhs, |
| StableBlockList<T, BlockCapacity, Allocator>& rhs) noexcept { |
| lhs.swap(rhs); |
| } |
| |
| } // namespace quiche |
| |
| #endif // QUICHE_COMMON_STABLE_BLOCK_LIST_H_ |