blob: f46646d0b1c8660cab746485e52c33b652fece55 [file]
// 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_