C Allocator Overview
Memory allocation in C++ typically involves direct use of the new,
and free operators. While flexible, these can incur performance
overhead, lead to fragmentation, and increase the risk of memory leaks or
dangling pointers in large applications.
The Allocator module in this library provides lightweight and efficient
allocation utilities implemented in pure C and declared in
allocator.hpp. The only dpendency this module has within the CSalt
library is the error.hpp file, making it suitable ifor integration
into larger systems.
Build-time configuration flags modify behavior depending on needs:
STATIC_ONLY— Disables all heap allocation. Only stack or static memory supplied by the application is permitted.
These features make the allocator suitable for environments requiring strict determinism and safety analysis, such as embedded and real-time systems or projects that require compliance to MISRA C++ standards.
Enums and Data Structures
The csalt++ library uses the following enum to help
indicate if the allocator is utilyzing statically allocated memory from
the stack or dynamically allocated memory from the heap.
enum MemType {
ALLOC_INVALID = 0,
STATIC = 1,
DYNAMIC = 2
};
Allocator Overview
- class Allocator
Abstract base class for custom memory allocators.
This class defines the interface for all custom allocators in the cslt namespace. It provides core allocation/deallocation methods along with optional tracking and management capabilities. Derived classes implement specific allocation strategies (heap, arena, pool, etc.).
The class supports:
Basic allocation and deallocation with optional zeroing
Aligned memory allocation
Reallocation with preservation of existing data
Memory tracking (size, capacity, utilization)
Optional save/restore checkpointing
Statistical reporting
// Using a derived class (HeapAllocator) cslt::HeapAllocator allocator; // Allocate memory auto result = allocator.alloc(1024, true); // 1KB zeroed if (result.hasValue()) { void* ptr = result.value(); // Use memory... allocator.return_element(ptr, 1024, allocator.default_alignment()); } // Query allocator properties if (allocator.owns_memory()) { size_t used = allocator.size(); size_t available = allocator.remaining(); }Note
This is an abstract class and cannot be instantiated directly. Use derived classes like HeapAllocator, ArenaAllocator, etc.
Subclassed by cslt::ArenaAllocator, cslt::BuddyAllocator, cslt::FreeListAllocator, cslt::HeapAllocator, cslt::PoolAllocator, cslt::SlabAllocator
Public Functions
- inline explicit Allocator(size_t alignment = alignof(max_align_t), size_t total_alloc = 0, MemType type = ALLOC_INVALID, bool owns_mem = false) noexcept
Construct an Allocator.
Initializes the allocator with the specified parameters. The total_alloc parameter represents the complete memory budget. Derived classes should calculate alloc_ by subtracting their overhead from total_alloc_.
// Derived class example: class ArenaAllocator : public Allocator { public: ArenaAllocator(size_t total_bytes) : Allocator(alignof(max_align_t), total_bytes, STATIC, true) { size_t overhead = sizeof(Header); alloc_ = total_alloc_ - overhead; // Calculate usable space buffer_ = malloc(total_alloc_); } };
- Parameters:
alignment – Default alignment for allocations (default: alignof(max_align_t))
total_alloc – Total memory budget in bytes (default: 0)
type – Memory type - STATIC or DYNAMIC (default: ALLOC_INVALID)
owns_mem – Whether this allocator owns its memory (default: false)
- virtual ~Allocator() noexcept = default
Virtual destructor.
Ensures proper cleanup of derived classes
- inline size_t default_alignment() const noexcept
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) = 0
Allocate memory.
cslt::HeapAllocator allocator; // Allocate 256 bytes, zeroed auto result = allocator.alloc(256, true); if (result.hasValue()) { void* ptr = result.value(); // Memory is guaranteed to be zeroed // ... use memory ... allocator.return_element(ptr, 256, allocator.default_alignment()); } else { // Handle allocation failure std::cerr << "Allocation failed\n"; }
- Parameters:
bytes – Number of bytes to allocate
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) = 0
Allocate aligned memory.
Allocates memory aligned to the specified boundary. The alignment must be a power of 2. If alignment is 0, uses default_alignment_.
cslt::HeapAllocator allocator; // Allocate 512 bytes aligned to 64-byte boundary auto result = allocator.alloc_aligned(512, 64, false); if (result.hasValue()) { void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // ... use memory ... allocator.return_element(ptr, 512, 64); }
- Parameters:
bytes – Number of bytes to allocate
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) = 0
Reallocate memory.
Resizes an existing allocation. The existing data is preserved up to min(old_bytes, new_bytes). If growing and zeroed is true, the additional bytes are zero-initialized. The old pointer is automatically freed. The returned pointer may differ from the input.
cslt::HeapAllocator allocator; // Initial allocation auto result1 = allocator.alloc(256, false); void* ptr = result1.value(); memset(ptr, 0xAA, 256); // Fill with pattern // Grow to 512 bytes, zero the new space auto result2 = allocator.realloc(ptr, 256, 512, true); if (result2.hasValue()) { void* new_ptr = result2.value(); // First 256 bytes still contain 0xAA // Bytes 256-511 are zeroed allocator.return_element(new_ptr, 512, allocator.default_alignment()); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) = 0
Reallocate aligned memory.
Like realloc(), but maintains the specified alignment.
cslt::HeapAllocator allocator; // Initial aligned allocation auto result1 = allocator.alloc_aligned(256, 64, false); void* ptr = result1.value(); // Grow to 512 bytes, maintaining 64-byte alignment auto result2 = allocator.realloc_aligned(ptr, 256, 512, 64, false); if (result2.hasValue()) { void* new_ptr = result2.value(); assert(reinterpret_cast<uintptr_t>(new_ptr) % 64 == 0); allocator.return_element(new_ptr, 512, 64); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual void return_element(void *ptr, size_t bytes, size_t alignment = alignof(max_align_t)) = 0
Return/free allocated memory.
Frees memory previously allocated by this allocator. For heap allocators, this calls operator delete or free(). For arena allocators, this may be a no-op or update bookkeeping. Always safe to call with nullptr (no-op).
cslt::HeapAllocator allocator; auto result = allocator.alloc_aligned(1024, 64, false); void* ptr = result.value(); // ... use memory ... // Free memory - must pass same size and alignment as allocation allocator.return_element(ptr, 1024, 64); // Safe to call with nullptr allocator.return_element(nullptr, 0, 0); // No-op
- Parameters:
ptr – Pointer to memory to free
bytes – Size of allocation in bytes
alignment – Alignment used for the allocation
- inline MemType memory_type() const noexcept
Get memory type.
Returns whether this allocator uses static or dynamic memory. STATIC = fixed buffer allocated at compile-time or startup DYNAMIC = memory from OS heap (malloc/new)
cslt::HeapAllocator heap_alloc; cslt::ArenaAllocator arena_alloc(1024); if (heap_alloc.memory_type() == cslt::DYNAMIC) { std::cout << "Heap allocator uses dynamic memory\n"; } if (arena_alloc.memory_type() == cslt::STATIC) { std::cout << "Arena allocator uses static memory\n"; }
- Returns:
MemType enum value (STATIC, DYNAMIC, or ALLOC_INVALID)
- virtual bool stats(char *buffer, size_t buffer_size) const = 0
Generate statistics string.
Writes human-readable statistics about the allocator to the provided buffer. Returns false if buffer is too small or null.
cslt::HeapAllocator allocator; char buffer[512]; if (allocator.stats(buffer, sizeof(buffer))) { printf("%s\n", buffer); // Output: // HeapAllocator Statistics: // Type: DYNAMIC // Default Alignment: 16 bytes // Memory Model: System Heap (operator new/delete) // ... } else { fprintf(stderr, "Buffer too small for statistics\n"); }
- Parameters:
buffer – Character buffer to write statistics to
buffer_size – Size of buffer in bytes
- Returns:
true if statistics were written successfully, false otherwise
- inline bool owns_memory() const noexcept
Check if allocator owns its memory.
Returns whether this allocator owns its underlying memory. true = allocator allocated and manages the memory (e.g., ArenaAllocator) false = allocator is a wrapper around system allocator (e.g., HeapAllocator)
cslt::HeapAllocator heap_alloc; cslt::ArenaAllocator arena_alloc(1024); if (heap_alloc.owns_memory()) { std::cout << "Heap allocator owns memory\n"; // Won't print } if (arena_alloc.owns_memory()) { std::cout << "Arena allocator owns memory\n"; // Will print }
- Returns:
true if allocator owns/manages its memory buffer, false otherwise
- inline virtual size_t size() const noexcept
Get current bytes in use.
Returns the amount of memory currently in use. For allocators that don’t track usage (like HeapAllocator), returns 0. For tracking allocators (like ArenaAllocator), returns actual usage.
cslt::ArenaAllocator allocator(1024); std::cout << "Initial size: " << allocator.size() << "\n"; // 0 auto result = allocator.alloc(256, false); std::cout << "After alloc: " << allocator.size() << "\n"; // 256 allocator.return_element(result.value(), 256, allocator.default_alignment()); std::cout << "After free: " << allocator.size() << "\n"; // 0 (if supported)
- Returns:
Number of bytes currently allocated and not freed
- inline virtual size_t remaining() const noexcept
Get remaining available bytes.
Returns alloc_ - size_, i.e., how much usable space is left. For allocators without fixed capacity (like HeapAllocator), returns 0. Includes underflow protection.
cslt::ArenaAllocator allocator(1024); std::cout << "Initial remaining: " << allocator.remaining() << "\n"; // ~1024 auto result = allocator.alloc(256, false); std::cout << "After alloc: " << allocator.remaining() << "\n"; // ~768 // Can use remaining to check if allocation will succeed if (allocator.remaining() >= 512) { auto result2 = allocator.alloc(512, false); }
- Returns:
Number of bytes still available for allocation
- inline virtual size_t allocated() const noexcept
Get total usable bytes.
Returns the total amount of memory available for user allocations, excluding any internal overhead or headers. This is calculated as: allocated() = total_alloc() - overhead
For allocators without fixed capacity (like HeapAllocator), returns 0. For allocators with fixed capacity (like ArenaAllocator), returns the usable capacity.
Relationship between memory functions:
total_alloc() = total memory including overhead
allocated() = usable memory (this function)
size() = currently used memory
cslt::ArenaAllocator allocator(1024); // Request 1024 total bytes size_t total = allocator.total_alloc(); // 1024 (what user requested) size_t usable = allocator.allocated(); // ~960 (after overhead) size_t overhead = total - usable; // ~64 (internal headers) std::cout << "Total budget: " << total << " bytes\n"; std::cout << "Usable memory: " << usable << " bytes\n"; std::cout << "Overhead: " << overhead << " bytes\n"; // After some allocations auto result = allocator.alloc(256, false); std::cout << "Allocated (capacity): " << allocator.allocated() << "\n"; // ~960 std::cout << "Used: " << allocator.size() << "\n"; // 256 std::cout << "Remaining: " << allocator.remaining() << "\n"; // ~704Note
This differs from size() which returns currently used memory. allocated() is the total capacity, size() is current usage.
- Returns:
Total usable memory in bytes (excluding overhead)
- inline virtual size_t total_alloc() const noexcept
Get total allocated bytes.
Returns the total memory allocation size including any internal overhead. This is the value passed to the constructor. For heap allocators without fixed capacity, returns 0.
cslt::ArenaAllocator allocator(1024); std::cout << "Total allocation: " << allocator.total_alloc() << "\n"; // 1024 std::cout << "Usable memory: " << allocator.remaining() << "\n"; // < 1024 size_t overhead = allocator.total_alloc() - allocator.remaining(); std::cout << "Overhead: " << overhead << " bytes\n";
- Returns:
Total memory budget in bytes (including overhead)
- inline virtual bool is_ptr(void *ptr) const
Check if pointer belongs to this allocator.
Checks if the given pointer was allocated by this allocator. Default implementation returns false (cannot verify). Allocators that own memory can override to provide actual verification.
cslt::ArenaAllocator allocator(1024); auto result = allocator.alloc(256, false); void* ptr = result.value(); void* external_ptr = malloc(256); if (allocator.is_ptr(ptr)) { std::cout << "ptr belongs to allocator\n"; } if (!allocator.is_ptr(external_ptr)) { std::cout << "external_ptr does not belong to allocator\n"; } free(external_ptr);
- Parameters:
ptr – Pointer to check
- Returns:
true if pointer was allocated by this allocator, false otherwise
- inline virtual bool is_ptr_sized(void *ptr, size_t bytes) const
Check if pointer and size are valid for this allocator.
Checks if the pointer was allocated by this allocator AND if it has the specified size. Default returns false. Useful for debugging and validation.
cslt::ArenaAllocator allocator(1024); auto result = allocator.alloc(256, false); void* ptr = result.value(); if (allocator.is_ptr_sized(ptr, 256)) { std::cout << "ptr is valid 256-byte allocation\n"; } if (!allocator.is_ptr_sized(ptr, 512)) { std::cout << "ptr is not a 512-byte allocation\n"; }
- Parameters:
ptr – Pointer to check
bytes – Expected size of allocation
- Returns:
true if pointer with given size is valid, false otherwise
- inline virtual void *save() const
Save allocator state checkpoint.
Saves the current state of the allocator for later restoration. Useful for implementing transactional semantics or temporary allocations. Default returns nullptr (not supported). Arena allocators typically return a position marker.
cslt::ArenaAllocator allocator(1024); // Save checkpoint void* checkpoint = allocator.save(); // Make temporary allocations auto temp1 = allocator.alloc(128, false); auto temp2 = allocator.alloc(256, false); // Restore to checkpoint (frees temp1 and temp2) if (checkpoint) { allocator.restore(checkpoint); std::cout << "Temporary allocations rolled back\n"; }
- Returns:
Opaque checkpoint pointer, or nullptr if not supported
- inline virtual bool restore(void *checkpoint)
Restore allocator to saved checkpoint.
Restores allocator state to a previous checkpoint, effectively freeing all allocations made after that checkpoint. Default returns false (not supported).
cslt::ArenaAllocator allocator(1024); auto permanent = allocator.alloc(128, false); void* checkpoint = allocator.save(); auto temp = allocator.alloc(256, false); std::cout << "Size with temp: " << allocator.size() << "\n"; // 384 if (allocator.restore(checkpoint)) { std::cout << "Size after restore: " << allocator.size() << "\n"; // 128 // temp is now invalid, permanent is still valid }
- Parameters:
checkpoint – Checkpoint pointer from previous save() call
- Returns:
true if restore succeeded, false if not supported or invalid checkpoint
- inline virtual bool reset(bool trim_extra_chunks = false)
Reset allocator to initial state.
Frees all allocations and resets the allocator to its initial state. Default is a no-op. Allocators that own memory override this to actually reset their state. After reset, all previous pointers are invalid.
cslt::ArenaAllocator allocator(1024); auto ptr1 = allocator.alloc(128, false); auto ptr2 = allocator.alloc(256, false); std::cout << "Before reset: " << allocator.size() << "\n"; // 384 allocator.reset(); std::cout << "After reset: " << allocator.size() << "\n"; // 0 // ptr1 and ptr2 are now INVALID - do not use! // Can allocate again from fresh state auto ptr3 = allocator.alloc(512, false);Protected Attributes
- size_t default_alignment_
Default alignment for memory allocations.
Alignment value used when no specific alignment is requested. Typically set to alignof(max_align_t) for maximum portability.
- size_t size_
Current bytes of memory in use.
Tracks how many bytes are currently allocated and not yet freed. For allocators that don’t track usage (like HeapAllocator), this remains 0.
- size_t alloc_
Bytes of usable memory available.
The amount of memory available for user allocations, excluding any internal overhead or headers. Calculated as: alloc_ = total_alloc_ - overhead
- size_t total_alloc_
Total bytes of allocated memory.
The total memory budget including overhead. This is what the user requested when creating the allocator. For heap allocators, this is 0.
- uint8_t mem_type_
Memory type (STATIC or DYNAMIC)
Stored as uint8_t, cast to/from MemType enum. STATIC = memory allocated at compile time or from a fixed buffer DYNAMIC = memory allocated from OS heap
- uint8_t owns_memory_
Whether allocator owns its memory.
Stored as uint8_t, cast to/from bool. true = allocator owns and manages the memory buffer false = allocator is a wrapper (e.g., HeapAllocator wraps operator new)
Heap Overview
- class HeapAllocator : public cslt::Allocator
Allocator wrapper around system heap (operator new/delete)
HeapAllocator is a simple wrapper around the system’s dynamic memory allocation facilities (operator new, operator delete, and platform-specific aligned allocation functions). It does not own or track memory - all allocations go directly to the OS heap.
Key characteristics:
Does not own memory (owns_memory() returns false)
Does not track allocations (size() returns 0)
No fixed capacity (total_alloc() and allocated() return 0)
Uses DYNAMIC memory type
Thread-safety depends on system allocator
No overhead beyond system allocator overhead
Use this allocator when:
You need a uniform interface with other allocators
You want to switch between allocator types at runtime
You need alignment support beyond standard new
You don’t need memory tracking or limits
Alignment behavior:
For alignments <= max_align_t: uses operator new
For alignments > max_align_t: uses platform-specific functions (posix_memalign on POSIX, _aligned_malloc on Windows)
Creates a HeapAllocator with specified default alignment.
HeapAllocator(size_t alignment = alignof(max_align_t)) - Construct with default alignment
alloc(size_t bytes, bool zeroed = false) - Allocate memory with default alignment
alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) - Allocate aligned memory
realloc(void* ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) - Resize allocation
realloc_aligned(void* ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) - Resize aligned allocation
return_element(void* ptr, size_t bytes, size_t alignment) - Free allocated memory
default_alignment() const - Get default alignment in bytes
memory_type() const - Returns DYNAMIC
owns_memory() const - Returns false (doesn’t own memory)
size() const - Returns 0 (doesn’t track usage)
allocated() const - Returns 0 (no fixed capacity)
remaining() const - Returns 0 (no capacity limit)
total_alloc() const - Returns 0 (no fixed allocation)
stats(char* buffer, size_t buffer_size) const - Generate statistics string
is_ptr(void* ptr) const - Returns false (cannot verify pointers)
is_ptr_sized(void* ptr, size_t bytes) const - Returns false (cannot verify)
save() const - Returns nullptr (no checkpoint support)
restore(void* checkpoint) - Returns false (no restore support)
reset() - No-op (cannot reset system heap)
{.c++} cslt::HeapAllocator allocator; // Allocate 1KB of zeroed memory auto result = allocator.alloc(1024, true); if (result.hasValue()) { void* ptr = result.value(); // Memory is guaranteed to be zeroed // Use memory... allocator.return_element(ptr, 1024, allocator.default_alignment()); }Public Functions
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) override
Allocate memory.
cslt::HeapAllocator allocator; // Allocate 256 bytes, zeroed auto result = allocator.alloc(256, true); if (result.hasValue()) { void* ptr = result.value(); // Memory is guaranteed to be zeroed // ... use memory ... allocator.return_element(ptr, 256, allocator.default_alignment()); } else { // Handle allocation failure std::cerr << "Allocation failed\n"; }
- Parameters:
bytes – Number of bytes to allocate
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) override
Allocate aligned memory.
Allocates memory aligned to the specified boundary. The alignment must be a power of 2. If alignment is 0, uses default_alignment_.
cslt::HeapAllocator allocator; // Allocate 512 bytes aligned to 64-byte boundary auto result = allocator.alloc_aligned(512, 64, false); if (result.hasValue()) { void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // ... use memory ... allocator.return_element(ptr, 512, 64); }
- Parameters:
bytes – Number of bytes to allocate
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) override
Reallocate memory.
Resizes an existing allocation. The existing data is preserved up to min(old_bytes, new_bytes). If growing and zeroed is true, the additional bytes are zero-initialized. The old pointer is automatically freed. The returned pointer may differ from the input.
cslt::HeapAllocator allocator; // Initial allocation auto result1 = allocator.alloc(256, false); void* ptr = result1.value(); memset(ptr, 0xAA, 256); // Fill with pattern // Grow to 512 bytes, zero the new space auto result2 = allocator.realloc(ptr, 256, 512, true); if (result2.hasValue()) { void* new_ptr = result2.value(); // First 256 bytes still contain 0xAA // Bytes 256-511 are zeroed allocator.return_element(new_ptr, 512, allocator.default_alignment()); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) override
Reallocate aligned memory.
Like realloc(), but maintains the specified alignment.
cslt::HeapAllocator allocator; // Initial aligned allocation auto result1 = allocator.alloc_aligned(256, 64, false); void* ptr = result1.value(); // Grow to 512 bytes, maintaining 64-byte alignment auto result2 = allocator.realloc_aligned(ptr, 256, 512, 64, false); if (result2.hasValue()) { void* new_ptr = result2.value(); assert(reinterpret_cast<uintptr_t>(new_ptr) % 64 == 0); allocator.return_element(new_ptr, 512, 64); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual void return_element(void *ptr, size_t bytes, size_t alignment = alignof(max_align_t)) override
Return/free allocated memory.
Frees memory previously allocated by this allocator. For heap allocators, this calls operator delete or free(). For arena allocators, this may be a no-op or update bookkeeping. Always safe to call with nullptr (no-op).
cslt::HeapAllocator allocator; auto result = allocator.alloc_aligned(1024, 64, false); void* ptr = result.value(); // ... use memory ... // Free memory - must pass same size and alignment as allocation allocator.return_element(ptr, 1024, 64); // Safe to call with nullptr allocator.return_element(nullptr, 0, 0); // No-op
- Parameters:
ptr – Pointer to memory to free
bytes – Size of allocation in bytes
alignment – Alignment used for the allocation
- virtual bool stats(char *buffer, size_t buffer_size) const
Generate statistics string.
Writes human-readable statistics about the allocator to the provided buffer. Returns false if buffer is too small or null.
cslt::HeapAllocator allocator; char buffer[512]; if (allocator.stats(buffer, sizeof(buffer))) { printf("%s\n", buffer); // Output: // HeapAllocator Statistics: // Type: DYNAMIC // Default Alignment: 16 bytes // Memory Model: System Heap (operator new/delete) // ... } else { fprintf(stderr, "Buffer too small for statistics\n"); }
- Parameters:
buffer – Character buffer to write statistics to
buffer_size – Size of buffer in bytes
- Returns:
true if statistics were written successfully, false otherwise
- inline virtual bool reset(bool trim_extra_chunks = false)
Reset allocator to initial state.
Frees all allocations and resets the allocator to its initial state. Default is a no-op. Allocators that own memory override this to actually reset their state. After reset, all previous pointers are invalid.
cslt::ArenaAllocator allocator(1024); auto ptr1 = allocator.alloc(128, false); auto ptr2 = allocator.alloc(256, false); std::cout << "Before reset: " << allocator.size() << "\n"; // 384 allocator.reset(); std::cout << "After reset: " << allocator.size() << "\n"; // 0 // ptr1 and ptr2 are now INVALID - do not use! // Can allocate again from fresh state auto ptr3 = allocator.alloc(512, false);
Arena Overview
- class ArenaAllocator : public cslt::Allocator
Fast, region-based memory allocator that allocates memory in chunks.
ArenaAllocator is a memory allocator that uses “arena” or “region” allocation strategy. It allocates memory sequentially from large chunks and does not support individual deallocation. All memory is freed together when the arena is reset or destroyed.
Key characteristics:
Very fast allocation (just pointer bumping)
No per-allocation metadata overhead
No fragmentation
No individual deallocation (use reset() instead)
Supports checkpoints for transactional rollback
Optional dynamic growth with additional chunks
Three initialization modes:
Heap(): Dynamically allocated arena that owns its memory
Stack(): Arena using a user-provided buffer (doesn’t own memory)
SubArena(): Arena allocated within a parent arena (doesn’t own memory)
Public Functions
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) override
Allocate memory.
cslt::HeapAllocator allocator; // Allocate 256 bytes, zeroed auto result = allocator.alloc(256, true); if (result.hasValue()) { void* ptr = result.value(); // Memory is guaranteed to be zeroed // ... use memory ... allocator.return_element(ptr, 256, allocator.default_alignment()); } else { // Handle allocation failure std::cerr << "Allocation failed\n"; }
- Parameters:
bytes – Number of bytes to allocate
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) override
Allocate aligned memory.
Allocates memory aligned to the specified boundary. The alignment must be a power of 2. If alignment is 0, uses default_alignment_.
cslt::HeapAllocator allocator; // Allocate 512 bytes aligned to 64-byte boundary auto result = allocator.alloc_aligned(512, 64, false); if (result.hasValue()) { void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // ... use memory ... allocator.return_element(ptr, 512, 64); }
- Parameters:
bytes – Number of bytes to allocate
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) override
Reallocate memory.
Resizes an existing allocation. The existing data is preserved up to min(old_bytes, new_bytes). If growing and zeroed is true, the additional bytes are zero-initialized. The old pointer is automatically freed. The returned pointer may differ from the input.
cslt::HeapAllocator allocator; // Initial allocation auto result1 = allocator.alloc(256, false); void* ptr = result1.value(); memset(ptr, 0xAA, 256); // Fill with pattern // Grow to 512 bytes, zero the new space auto result2 = allocator.realloc(ptr, 256, 512, true); if (result2.hasValue()) { void* new_ptr = result2.value(); // First 256 bytes still contain 0xAA // Bytes 256-511 are zeroed allocator.return_element(new_ptr, 512, allocator.default_alignment()); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) override
Reallocate aligned memory.
Like realloc(), but maintains the specified alignment.
cslt::HeapAllocator allocator; // Initial aligned allocation auto result1 = allocator.alloc_aligned(256, 64, false); void* ptr = result1.value(); // Grow to 512 bytes, maintaining 64-byte alignment auto result2 = allocator.realloc_aligned(ptr, 256, 512, 64, false); if (result2.hasValue()) { void* new_ptr = result2.value(); assert(reinterpret_cast<uintptr_t>(new_ptr) % 64 == 0); allocator.return_element(new_ptr, 512, 64); }
- Parameters:
ptr – Pointer to existing allocation
old_bytes – Size of existing allocation in bytes
new_bytes – Desired new size in bytes
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize new bytes when growing (default: false)
- Returns:
Expected<void*> containing pointer to reallocated memory or error
- virtual bool is_ptr(void *ptr) const override
Check if pointer belongs to this allocator.
Checks if the given pointer was allocated by this allocator. Default implementation returns false (cannot verify). Allocators that own memory can override to provide actual verification.
cslt::ArenaAllocator allocator(1024); auto result = allocator.alloc(256, false); void* ptr = result.value(); void* external_ptr = malloc(256); if (allocator.is_ptr(ptr)) { std::cout << "ptr belongs to allocator\n"; } if (!allocator.is_ptr(external_ptr)) { std::cout << "external_ptr does not belong to allocator\n"; } free(external_ptr);
- Parameters:
ptr – Pointer to check
- Returns:
true if pointer was allocated by this allocator, false otherwise
- virtual bool is_ptr_sized(void *ptr, size_t bytes) const override
Check if pointer and size are valid for this allocator.
Checks if the pointer was allocated by this allocator AND if it has the specified size. Default returns false. Useful for debugging and validation.
cslt::ArenaAllocator allocator(1024); auto result = allocator.alloc(256, false); void* ptr = result.value(); if (allocator.is_ptr_sized(ptr, 256)) { std::cout << "ptr is valid 256-byte allocation\n"; } if (!allocator.is_ptr_sized(ptr, 512)) { std::cout << "ptr is not a 512-byte allocation\n"; }
- Parameters:
ptr – Pointer to check
bytes – Expected size of allocation
- Returns:
true if pointer with given size is valid, false otherwise
- virtual void return_element(void *ptr, size_t bytes, size_t alignment = alignof(max_align_t)) override
Return/free allocated memory.
Frees memory previously allocated by this allocator. For heap allocators, this calls operator delete or free(). For arena allocators, this may be a no-op or update bookkeeping. Always safe to call with nullptr (no-op).
cslt::HeapAllocator allocator; auto result = allocator.alloc_aligned(1024, 64, false); void* ptr = result.value(); // ... use memory ... // Free memory - must pass same size and alignment as allocation allocator.return_element(ptr, 1024, 64); // Safe to call with nullptr allocator.return_element(nullptr, 0, 0); // No-op
- Parameters:
ptr – Pointer to memory to free
bytes – Size of allocation in bytes
alignment – Alignment used for the allocation
- virtual bool reset(bool trim_extra_chunks = false) override
Reset allocator to initial state.
Frees all allocations and resets the allocator to its initial state. Default is a no-op. Allocators that own memory override this to actually reset their state. After reset, all previous pointers are invalid.
cslt::ArenaAllocator allocator(1024); auto ptr1 = allocator.alloc(128, false); auto ptr2 = allocator.alloc(256, false); std::cout << "Before reset: " << allocator.size() << "\n"; // 384 allocator.reset(); std::cout << "After reset: " << allocator.size() << "\n"; // 0 // ptr1 and ptr2 are now INVALID - do not use! // Can allocate again from fresh state auto ptr3 = allocator.alloc(512, false);
- virtual void *save() const override
Save allocator state checkpoint.
Saves the current state of the allocator for later restoration. Useful for implementing transactional semantics or temporary allocations. Default returns nullptr (not supported). Arena allocators typically return a position marker.
cslt::ArenaAllocator allocator(1024); // Save checkpoint void* checkpoint = allocator.save(); // Make temporary allocations auto temp1 = allocator.alloc(128, false); auto temp2 = allocator.alloc(256, false); // Restore to checkpoint (frees temp1 and temp2) if (checkpoint) { allocator.restore(checkpoint); std::cout << "Temporary allocations rolled back\n"; }
- Returns:
Opaque checkpoint pointer, or nullptr if not supported
- virtual bool restore(void *checkpoint) override
Restore allocator to saved checkpoint.
Restores allocator state to a previous checkpoint, effectively freeing all allocations made after that checkpoint. Default returns false (not supported).
cslt::ArenaAllocator allocator(1024); auto permanent = allocator.alloc(128, false); void* checkpoint = allocator.save(); auto temp = allocator.alloc(256, false); std::cout << "Size with temp: " << allocator.size() << "\n"; // 384 if (allocator.restore(checkpoint)) { std::cout << "Size after restore: " << allocator.size() << "\n"; // 128 // temp is now invalid, permanent is still valid }
- Parameters:
checkpoint – Checkpoint pointer from previous save() call
- Returns:
true if restore succeeded, false if not supported or invalid checkpoint
- virtual size_t remaining() const noexcept override
Get remaining available bytes.
Returns alloc_ - size_, i.e., how much usable space is left. For allocators without fixed capacity (like HeapAllocator), returns 0. Includes underflow protection.
cslt::ArenaAllocator allocator(1024); std::cout << "Initial remaining: " << allocator.remaining() << "\n"; // ~1024 auto result = allocator.alloc(256, false); std::cout << "After alloc: " << allocator.remaining() << "\n"; // ~768 // Can use remaining to check if allocation will succeed if (allocator.remaining() >= 512) { auto result2 = allocator.alloc(512, false); }
- Returns:
Number of bytes still available for allocation
- virtual bool stats(char *buffer, size_t buffer_size) const override
Generate statistics string.
Writes human-readable statistics about the allocator to the provided buffer. Returns false if buffer is too small or null.
cslt::HeapAllocator allocator; char buffer[512]; if (allocator.stats(buffer, sizeof(buffer))) { printf("%s\n", buffer); // Output: // HeapAllocator Statistics: // Type: DYNAMIC // Default Alignment: 16 bytes // Memory Model: System Heap (operator new/delete) // ... } else { fprintf(stderr, "Buffer too small for statistics\n"); }
- Parameters:
buffer – Character buffer to write statistics to
buffer_size – Size of buffer in bytes
- Returns:
true if statistics were written successfully, false otherwise
Public Static Functions
- static Expected<cslt::UniquePtr<ArenaAllocator, ArenaDeleter>> WithBuddy(BuddyAllocator &buddy, size_t bytes, size_t base_align_in = alignof(max_align_t))
Create a fixed-capacity arena backed by a BuddyAllocator allocation.
Performs a single allocation from
buddyand constructs the ArenaAllocator in-place at the returned base pointer. The arena is fixed-capacity:
No resizing/growth (resize disabled)
The buddy allocator retains ownership of the backing region
Destruction returns the entire region back to the buddy allocator
Layout within the buddy allocation: [ArenaAllocator][padding][Chunk][padding][data…]
ArenaAllocator is placed at the beginning of the buddy region.
Chunk header is aligned to alignof(Chunk).
Data region is aligned to base_align (>= alignof(max_align_t), pow2).
See also
ArenaDeleter Returns memory to buddy for WithBuddy arenas
Warning
The returned arena lives inside buddy-owned memory. Never free it with ::operator delete. Always use ArenaDeleter / UniquePtr cleanup.
- Parameters:
buddy – Buddy allocator to allocate the arena region from (must not be nullptr)
bytes – Total bytes to request from the buddy allocator for the entire arena region
base_align_in – Per-arena base alignment for allocations (0 = default)
- Returns:
Expected containing UniquePtr to arena on success, or error on failure
- Return values:
Expected – with UniquePtr on success
Expected – with ArgumentError if buddy is null or bytes too small
Expected – with MemoryError if buddy allocation fails
Expected – with AlignmentError if alignment normalization fails
Expected – with LengthOverflowError on overflow in address arithmetic
ArenaDeleter
An ArenaAllocator class can have it memory manually freed or be passed
between scopes. However, if the user does not manually free the memory, it
will be automatically freed after it leaves its originating scope.
The ArenaDeleter struct is a data structure used to destroy Arena memory
after it goes out of scope. While this is a publically available struct, the
struct is automatically invoked via a UniquePtr when an ArenaAllocator
is initialized, and the user does not need to interact with it.
- struct ArenaDeleter
Custom deleter for ArenaAllocator unique pointers.
This deleter properly cleans up ArenaAllocator instances created by factory methods (Heap, Stack, SubArena). It:
Calls the arena’s destructor (frees additional chunks)
Frees the base memory only if the arena owns it (DYNAMIC only)
Memory ownership:
DYNAMIC (Heap): Owns memory → frees with ::operator delete
STATIC (Stack): User owns buffer → doesn’t free
SUB (SubArena): Parent owns memory → doesn’t free
Pool Overview
- class PoolAllocator : public cslt::Allocator
Fixed-size block allocator with O(1) allocation and deallocation.
PoolAllocator manages memory as a collection of uniformly-sized blocks, providing constant-time allocation and deallocation through free-list recycling. All allocations return blocks of exactly the same size, making it ideal for object pools and frequently allocated/deallocated data structures.
The pool carves blocks from contiguous memory slices backed by an arena. Freed blocks are returned to a linked free-list for O(1) reuse. Optionally, the pool can request additional slices when capacity is exhausted.
See also
Heap() Create pool with owned heap-allocated arena
See also
WithArena() Create pool sharing an existing arena
See also
Stack() Create pool using user-provided buffer
- Basic Usage
// Create pool for 256-byte blocks auto pool = PoolAllocator::Heap(256, 32).value(); // Allocate block auto ptr = pool->alloc_pool(false); // Return block for reuse pool->return_element(ptr.value(), 256);- Object Pool Pattern
struct Particle { float x, y, z; }; auto pool = PoolAllocator::Heap(sizeof(Particle), 1000).value(); // Allocate and construct void* mem = pool->alloc_pool().value(); Particle* p = new (mem) Particle(); // Destroy and return p->~Particle(); pool->return_element(mem, sizeof(Particle));Note
Block size must be at least sizeof(void*) for free-list management
Note
Not thread-safe - requires external synchronization for concurrent access
Public Functions
- ~PoolAllocator() noexcept override
Destructor - cleans up pool and optionally arena.
If the pool owns its arena, destroys the arena. Otherwise, just cleans up pool state. All allocated blocks become invalid.
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) override
Allocate memory.
cslt::HeapAllocator allocator; // Allocate 256 bytes, zeroed auto result = allocator.alloc(256, true); if (result.hasValue()) { void* ptr = result.value(); // Memory is guaranteed to be zeroed // ... use memory ... allocator.return_element(ptr, 256, allocator.default_alignment()); } else { // Handle allocation failure std::cerr << "Allocation failed\n"; }
- Parameters:
bytes – Number of bytes to allocate
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) override
Allocate aligned memory.
Allocates memory aligned to the specified boundary. The alignment must be a power of 2. If alignment is 0, uses default_alignment_.
cslt::HeapAllocator allocator; // Allocate 512 bytes aligned to 64-byte boundary auto result = allocator.alloc_aligned(512, 64, false); if (result.hasValue()) { void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // ... use memory ... allocator.return_element(ptr, 512, 64); }
- Parameters:
bytes – Number of bytes to allocate
alignment – Required alignment in bytes (must be power of 2)
zeroed – If true, zero-initialize the allocated memory (default: false)
- Returns:
Expected<void*> containing pointer to allocated memory or error
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) override
Realloc not supported for pools.
Pools allocate fixed-size blocks and cannot resize them. To “resize”, allocate a new block, copy data, and free the old.
- Returns:
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) override
Realloc aligned not supported for pools.
- Returns:
- virtual void return_element(void *ptr, size_t bytes, size_t alignment = alignof(max_align_t)) override
Return/free allocated memory.
Frees memory previously allocated by this allocator. For heap allocators, this calls operator delete or free(). For arena allocators, this may be a no-op or update bookkeeping. Always safe to call with nullptr (no-op).
cslt::HeapAllocator allocator; auto result = allocator.alloc_aligned(1024, 64, false); void* ptr = result.value(); // ... use memory ... // Free memory - must pass same size and alignment as allocation allocator.return_element(ptr, 1024, 64); // Safe to call with nullptr allocator.return_element(nullptr, 0, 0); // No-op
- Parameters:
ptr – Pointer to memory to free
bytes – Size of allocation in bytes
alignment – Alignment used for the allocation
- virtual bool reset(bool trim_extra_chunks = false) override
Reset allocator to initial state.
Frees all allocations and resets the allocator to its initial state. Default is a no-op. Allocators that own memory override this to actually reset their state. After reset, all previous pointers are invalid.
cslt::ArenaAllocator allocator(1024); auto ptr1 = allocator.alloc(128, false); auto ptr2 = allocator.alloc(256, false); std::cout << "Before reset: " << allocator.size() << "\n"; // 384 allocator.reset(); std::cout << "After reset: " << allocator.size() << "\n"; // 0 // ptr1 and ptr2 are now INVALID - do not use! // Can allocate again from fresh state auto ptr3 = allocator.alloc(512, false);
- virtual void *save() const override
Save allocator state checkpoint.
Saves the current state of the allocator for later restoration. Useful for implementing transactional semantics or temporary allocations. Default returns nullptr (not supported). Arena allocators typically return a position marker.
cslt::ArenaAllocator allocator(1024); // Save checkpoint void* checkpoint = allocator.save(); // Make temporary allocations auto temp1 = allocator.alloc(128, false); auto temp2 = allocator.alloc(256, false); // Restore to checkpoint (frees temp1 and temp2) if (checkpoint) { allocator.restore(checkpoint); std::cout << "Temporary allocations rolled back\n"; }
- Returns:
Opaque checkpoint pointer, or nullptr if not supported
- virtual bool restore(void *checkpoint) override
Restore pool to saved checkpoint.
Restores pool to checkpoint state. All blocks allocated after the checkpoint become invalid. The checkpoint is freed.
- Parameters:
checkpoint – Checkpoint from previous save()
- Returns:
true on success, false on error
- virtual bool is_ptr(void *ptr) const override
Check if pointer was allocated by this pool.
Delegates to the underlying arena’s is_ptr() check.
- Parameters:
ptr – Pointer to check
- Returns:
true if pointer is from this pool’s arena
- virtual bool is_ptr_sized(void *ptr, size_t bytes) const override
Check if pointer with size is valid for this pool.
- Parameters:
ptr – Pointer to check
bytes – Size to validate (should be block_size)
- Returns:
true if pointer and size are valid
- virtual bool stats(char *buffer, size_t buffer_size) const override
Generate pool statistics.
Writes detailed pool statistics including block size, total blocks, free blocks, utilization, and arena information.
- Parameters:
buffer – Buffer to write statistics to
buffer_size – Size of buffer
- Returns:
true if statistics written successfully
- inline size_t block_size() const noexcept
Get the pool’s fixed block size.
- Returns:
Block size in bytes
- inline size_t stride() const noexcept
Get the actual stride (aligned block size)
- Returns:
Stride in bytes
- inline size_t total_blocks() const noexcept
Get total number of blocks available.
- Returns:
Total blocks (allocated + free)
- inline size_t free_blocks() const noexcept
Get number of free blocks available.
- Returns:
Free blocks in free-list
- inline size_t allocated_blocks() const noexcept
Get number of allocated blocks.
- Returns:
Blocks currently in use
- inline bool can_grow() const noexcept
Check if pool can grow.
- Returns:
true if growth enabled
- void toggle_grow(bool enable) noexcept
Enable or disable pool growth.
Only works if pool owns arena or arena supports growth.
- Parameters:
enable – If true, enable growth; if false, disable
Pool Deleter
A PoolAllocator class can have it memory manually freed or be passed
between scopes. However, if the user does not manually free the memory, it
will be automatically freed after it leaves its originating scope.
The PoolDeleter struct is a data structure used to destroy Arena memory
after it goes out of scope. While this is a publically available struct, the
struct is automatically invoked via a UniquePtr when a PoolAllocator
is initialized, and the user does not need to interact with it.
- struct PoolDeleter
Custom deleter for PoolAllocator unique pointers.
Similar to ArenaDeleter, properly cleans up PoolAllocator instances created by factory methods. Handles ownership appropriately:
DYNAMIC pools that own their arena: frees the pool and arena
Pools with borrowed arenas: only frees the pool structure
This deleter properly cleans up PoolAllocator instances created by factory methods (Heap, WithArena, Stack). It handles the complex ownership relationships between pools and their backing arenas:
Calls the pool’s destructor (cleans up pool state)
Conditionally destroys the arena based on ownership
Arena ownership patterns:
Heap(): Pool owns arena → ArenaDeleter frees arena and memory
WithArena(): Pool borrows arena → Arena not deleted (caller owns it)
Stack(): Pool owns arena object → ArenaDeleter destructs arena (but doesn’t free buffer, user owns it)
The deleter respects the
owns_arena_flag to determine whether the pool created its own arena (Heap/Stack) or borrowed one (WithArena).Note
This deleter is noexcept and safe to call with nullptr.
Free List Overview
- class FreeListAllocator : public cslt::Allocator
Variable-size block allocator with automatic coalescing to reduce fragmentation.
FreeListAllocator manages variable-sized allocations from a contiguous memory region using a linked list of free blocks. Unlike PoolAllocator which handles fixed-size blocks, FreeListAllocator can allocate blocks of any size up to available capacity. Freed blocks are automatically coalesced with adjacent free blocks to reduce external fragmentation.
Each allocated block carries a small header storing its size and alignment offset. Free blocks maintain a linked list for efficient reuse. The allocator uses a best-fit search strategy to find suitable free blocks.
See also
Heap() Create freelist with owned heap-allocated arena
See also
WithArena() Create freelist sharing an existing arena
See also
Stack() Create freelist using user-provided buffer
- Basic Usage
// Create freelist with 4KB capacity auto freelist = FreeListAllocator::Heap(4096, 0, false).value(); // Allocate variable-sized blocks auto ptr1 = freelist->alloc(256, false); auto ptr2 = freelist->alloc(512, false); auto ptr3 = freelist->alloc(128, false); // Free blocks (automatically coalesces adjacent blocks) freelist->return_element(ptr2.value(), 512); freelist->return_element(ptr1.value(), 256);- Resize Support
auto freelist = FreeListAllocator::Heap(8192).value(); // Initial allocation auto ptr = freelist->alloc(128, false).value(); // Grow allocation (allocates new block, copies data, frees old) auto new_ptr = freelist->realloc(ptr, 128, 512, false); // Can also resize with specific alignment auto aligned = freelist->realloc_aligned(new_ptr.value(), 512, 1024, 64, true);Note
Allocations have small per-block overhead for metadata (FreeListHeader)
Note
Not thread-safe - requires external synchronization for concurrent access
Note
Best suited for general-purpose allocation with varying sizes
Public Functions
- ~FreeListAllocator() noexcept override
Destructor for FreeListAllocator.
Cleans up the freelist allocator’s internal state. The destructor is intentionally minimal because memory management is handled by the FreeListDeleter custom deleter, which determines whether to destroy the backing arena based on ownership.
The destructor simply nulls internal pointers to prevent dangling references. Actual memory deallocation (if any) is performed by FreeListDeleter after calling this destructor.
See also
FreeListDeleter For the cleanup logic that follows destruction
Note
This destructor is called by FreeListDeleter, not directly by users. Users should let UniquePtr manage the freelist lifetime.
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) override
Allocate variable-size block from freelist.
Allocates a block of the requested size from the freelist using a first-fit search strategy. The allocator searches the free list for the first block large enough to satisfy the request, accounting for alignment padding and header overhead.
Internally, this delegates to alloc_aligned() using the freelist’s default alignment. Each allocated block carries a small header (FreeListHeader) storing its total size and offset for proper deallocation.
If a free block is larger than needed, it may be split into an allocated portion and a new smaller free block. If the remainder would be too small (< sizeof(FreeBlock)), the entire block is consumed to avoid creating unusable fragments.
See also
alloc_aligned() For allocations with specific alignment requirements
See also
return_element() To free allocated blocks
See also
realloc() To resize existing allocations
See also
remaining() To check available capacity
- Example - Basic allocation
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Allocate different sizes auto ptr1 = freelist->alloc(256, false); if (!ptr1.hasValue()) { std::cerr << "Allocation failed: " << ptr1.error().what() << std::endl; return; } auto ptr2 = freelist->alloc(512, true); // Zero-initialized auto ptr3 = freelist->alloc(128, false); // Use allocations... uint8_t* data = static_cast<uint8_t*>(ptr1.value()); data[0] = 42; // Free when done freelist->return_element(ptr1.value(), 256); freelist->return_element(ptr2.value(), 512); freelist->return_element(ptr3.value(), 128);- Example - Checking capacity
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); size_t large_size = 8192; if (freelist->remaining() >= large_size) { auto ptr = freelist->alloc(large_size, false); // Will succeed } else { std::cerr << "Insufficient space: need " << large_size << ", have " << freelist->remaining() << std::endl; }- Example - Zero-initialized allocation
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // Allocate zero-initialized buffer auto result = freelist->alloc(1024, true); if (result.hasValue()) { uint8_t* buffer = static_cast<uint8_t*>(result.value()); // All 1024 bytes guaranteed to be 0 assert(buffer[0] == 0); assert(buffer[1023] == 0); }Note
The actual memory consumed may exceed ‘bytes’ due to:
FreeListHeader (stored before user pointer)
Alignment padding
Full block consumption when remainder is too small
Note
Allocated blocks must be returned via return_element() for reuse. Adjacent freed blocks are automatically coalesced to reduce fragmentation.
Warning
Passing bytes larger than available capacity will fail. Check remaining() before large allocations if needed.
- Parameters:
bytes – Number of bytes to allocate. Must be greater than 0.
zeroed – If true, zero-initialize the allocated block before returning.
- Return values:
Expected – with void* pointer on success
Expected – with ArgumentError if bytes == 0
Expected – with AlignmentError if alignment validation fails
Expected – with CapacityOverflowError if no suitable free block available
- Returns:
Expected containing pointer to allocated block on success, or error
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) override
Allocate variable-size block with specific alignment.
Allocates a block of the requested size with the specified alignment. The allocator searches the free list for a block large enough to accommodate:
FreeListHeader metadata
Alignment padding to satisfy the alignment requirement
The requested user bytes
The returned pointer is guaranteed to be aligned to at least the requested alignment (or the freelist’s default alignment, whichever is greater). Alignment padding increases internal fragmentation but ensures correct data alignment for hardware requirements.
If the requested alignment is 0 or less than the freelist’s default, the default alignment is used. Non-power-of-two alignments are rounded up to the next power of 2.
See also
alloc() For allocations with default alignment
See also
realloc_aligned() To resize with specific alignment
See also
return_element() To free allocated blocks
- Example - Cache-line aligned allocation
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); // Allocate 256 bytes aligned to 64-byte cache line auto result = freelist->alloc_aligned(256, 64, false); if (!result.hasValue()) { std::cerr << "Aligned allocation failed" << std::endl; return; } void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // Use aligned memory... freelist->return_element(ptr, 256);- Example - SIMD-friendly allocation
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Allocate buffer for AVX operations (32-byte alignment) auto buffer = freelist->alloc_aligned(1024, 32, true); if (buffer.hasValue()) { float* simd_data = static_cast<float*>(buffer.value()); // Safe for AVX intrinsics // __m256 vec = _mm256_load_ps(simd_data); freelist->return_element(buffer.value(), 1024); }- Example - Page-aligned allocation
auto freelist = cslt::FreeListAllocator::Heap(65536, 0, false).value(); // Allocate page-aligned memory (4KB pages) auto result = freelist->alloc_aligned(8192, 4096, false); if (result.hasValue()) { void* page_aligned = result.value(); // Pointer is aligned to 4KB boundary assert(reinterpret_cast<uintptr_t>(page_aligned) % 4096 == 0); // Could be used for mmap-like operations or DMA freelist->return_element(page_aligned, 8192); }- Example - Mixing alignments
auto freelist = cslt::FreeListAllocator::Heap(16384, 16, false).value(); // Default alignment (16 bytes) auto ptr1 = freelist->alloc(256, false); // Custom alignment (128 bytes) auto ptr2 = freelist->alloc_aligned(256, 128, false); // Alignment less than default - uses default (16 bytes) auto ptr3 = freelist->alloc_aligned(256, 8, false); // Actually 16-byte aligned // Verify alignments assert(reinterpret_cast<uintptr_t>(ptr1.value()) % 16 == 0); assert(reinterpret_cast<uintptr_t>(ptr2.value()) % 128 == 0); assert(reinterpret_cast<uintptr_t>(ptr3.value()) % 16 == 0);Note
Stricter alignment may consume significantly more memory due to padding. For example, a 64-byte allocation with 256-byte alignment may consume up to ~320 bytes total.
Note
The allocated block must be freed via return_element(), not free().
Warning
Requesting alignment stricter than the freelist’s base alignment increases fragmentation and reduces effective capacity.
- Parameters:
bytes – Number of bytes to allocate. Must be greater than 0.
alignment – Required alignment for the returned pointer. If 0, uses freelist’s default alignment. Must be power of 2. The effective alignment is always at least the freelist’s default alignment.
zeroed – If true, zero-initialize the allocated block before returning.
- Return values:
Expected – with void* pointer (aligned to requested alignment) on success
Expected – with ArgumentError if bytes == 0
Expected – with AlignmentError if alignment is not power of 2
Expected – with CapacityOverflowError if no suitable free block available
- Returns:
Expected containing aligned pointer on success, or error
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) override
Resize an existing allocation.
Resizes an allocation following standard realloc() semantics:
**NULL pointer**: Behaves like alloc(new_bytes, zeroed) **Shrink or same size** (new_bytes <= old_bytes): - Returns the original pointer unchanged - No memory is freed or reallocated - Freelist does not support in-place shrinking **Grow** (new_bytes > old_bytes): 1. Allocates new block of new_bytes 2. Copies first old_bytes from old block to new block 3. Zero-fills [old_bytes, new_bytes) if zeroed == true 4. Returns old block to freelist via return_element() 5. Returns pointer to new block The freelist never performs in-place growth; all expansions require allocate-copy-free semantics. This ensures proper alignment and allows coalescing of the old block.See also
alloc() For initial allocations
See also
realloc_aligned() To resize with specific alignment
See also
return_element() To free allocations
- Example - Growing a buffer
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Initial allocation auto result = freelist->alloc(128, false); if (!result.hasValue()) { return; } void* ptr = result.value(); memcpy(ptr, "Hello", 5); // Store some data // Need more space - grow to 512 bytes auto new_result = freelist->realloc(ptr, 128, 512, false); if (!new_result.hasValue()) { std::cerr << "Realloc failed: " << new_result.error().what() << std::endl; freelist->return_element(ptr, 128); // Free original on failure return; } void* new_ptr = new_result.value(); // First 128 bytes copied, "Hello" still there assert(memcmp(new_ptr, "Hello", 5) == 0); // ptr is now invalid, use new_ptr freelist->return_element(new_ptr, 512);- Example - Growing with zero-fill
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // Allocate 64 bytes void* ptr = freelist->alloc(64, false).value(); uint8_t* data = static_cast<uint8_t*>(ptr); data[0] = 42; // Grow to 256 bytes, zero new region auto new_ptr = freelist->realloc(ptr, 64, 256, true).value(); uint8_t* new_data = static_cast<uint8_t*>(new_ptr); // Old data preserved assert(new_data[0] == 42); // New region zeroed assert(new_data[64] == 0); assert(new_data[255] == 0); freelist->return_element(new_ptr, 256);- Example - Shrinking (no-op)
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); void* ptr = freelist->alloc(512, false).value(); // Try to shrink to 256 bytes auto result = freelist->realloc(ptr, 512, 256, false); // Returns same pointer (no reallocation) assert(result.value() == ptr); // Still need to free with original size freelist->return_element(ptr, 512);- Example - NULL pointer (behaves like alloc)
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // NULL pointer - allocates new block auto result = freelist->realloc(nullptr, 0, 256, true); // Equivalent to alloc(256, true) assert(result.hasValue()); freelist->return_element(result.value(), 256);- Example - Dynamic array growth pattern
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); void* array = nullptr; size_t capacity = 0; size_t count = 0; // Add elements, growing as needed for (int i = 0; i < 100; ++i) { if (count >= capacity) { // Grow by 2x size_t new_capacity = capacity == 0 ? 16 : capacity * 2; size_t new_bytes = new_capacity * sizeof(int); size_t old_bytes = capacity * sizeof(int); auto result = freelist->realloc(array, old_bytes, new_bytes, false); if (!result.hasValue()) { break; // Out of memory } array = result.value(); capacity = new_capacity; } static_cast<int*>(array)[count++] = i; } // Cleanup if (array) { freelist->return_element(array, capacity * sizeof(int)); }Note
Growth always allocates a new block - the returned pointer will be different from the original when new_bytes > old_bytes.
Note
The caller must track old_bytes accurately. Incorrect values may result in data corruption (too small) or invalid memory access (too large).
Note
Old pointer becomes invalid after successful growth. Do not use it.
Warning
Passing incorrect old_bytes can corrupt data or crash. The freelist cannot validate the size parameter.
- Parameters:
ptr – Existing pointer previously returned by alloc() or alloc_aligned(). May be nullptr, in which case this behaves like alloc(new_bytes, zeroed).
old_bytes – Size of the existing allocation in bytes. The freelist does not track user sizes internally; caller must provide accurate value.
new_bytes – Desired new size in bytes. Must be greater than 0.
zeroed – If true and the allocation grows, the newly added region [old_bytes, new_bytes) is zero-initialized.
- Return values:
Expected – with void* pointer on success (may be same as ptr or different)
Expected – with ArgumentError if new_bytes == 0
Expected – with CapacityOverflowError if growth fails due to insufficient space
- Returns:
Expected containing pointer to resized allocation on success, or error
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) override
Resize an existing allocation with specific alignment.
Resizes an allocation with alignment-aware semantics:
**NULL pointer**: Behaves like alloc_aligned(new_bytes, alignment, zeroed) **Shrink or same size** (new_bytes <= old_bytes): - Returns the original pointer unchanged - No reallocation occurs (even if alignment differs) **Grow** (new_bytes > old_bytes): 1. Allocates new aligned block of new_bytes with requested alignment 2. Copies first old_bytes from old block to new block 3. Zero-fills [old_bytes, new_bytes) if zeroed == true 4. Returns old block to freelist 5. Returns pointer to new aligned block If the original block was allocated with weaker alignment than requested, the new block will necessarily move to satisfy the stricter alignment requirement.See also
realloc() For resizing with default alignment
See also
alloc_aligned() For initial aligned allocations
See also
return_element() To free allocations
- Example - Growing with alignment preserved
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); // Allocate 128 bytes with 64-byte alignment auto result = freelist->alloc_aligned(128, 64, false); void* ptr = result.value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 64 == 0); // Grow to 512 bytes, preserving 64-byte alignment auto new_result = freelist->realloc_aligned(ptr, 128, 512, 64, false); void* new_ptr = new_result.value(); // New block also 64-byte aligned assert(reinterpret_cast<uintptr_t>(new_ptr) % 64 == 0); freelist->return_element(new_ptr, 512);- Example - SIMD buffer growth
auto freelist = cslt::FreeListAllocator::Heap(32768, 0, false).value(); // Start with small AVX-aligned buffer void* buffer = freelist->alloc_aligned(256, 32, true).value(); float* simd_buffer = static_cast<float*>(buffer); // Fill with data for (int i = 0; i < 64; ++i) { simd_buffer[i] = static_cast<float>(i); } // Need more space - grow to 1024 bytes auto new_buffer = freelist->realloc_aligned( buffer, 256, 1024, 32, true // Preserve 32-byte alignment, zero new region ).value(); float* new_simd = static_cast<float*>(new_buffer); // Old data preserved assert(new_simd[0] == 0.0f); assert(new_simd[63] == 63.0f); // New region zeroed assert(new_simd[64] == 0.0f); freelist->return_element(new_buffer, 1024);- Example - Changing alignment on realloc
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); // Start with default alignment void* ptr = freelist->alloc(128, false).value(); // Grow and require stricter alignment (128-byte) auto new_ptr = freelist->realloc_aligned(ptr, 128, 512, 128, false).value(); // New allocation is 128-byte aligned assert(reinterpret_cast<uintptr_t>(new_ptr) % 128 == 0); freelist->return_element(new_ptr, 512);- Example - Page-aligned buffer expansion
auto freelist = cslt::FreeListAllocator::Heap(65536, 0, false).value(); // Allocate page-aligned buffer for DMA void* dma_buffer = freelist->alloc_aligned(4096, 4096, false).value(); // Need to expand DMA buffer auto expanded = freelist->realloc_aligned( dma_buffer, 4096, 8192, 4096, false // Maintain page alignment ); if (expanded.hasValue()) { void* new_dma = expanded.value(); // Still page-aligned for DMA operations assert(reinterpret_cast<uintptr_t>(new_dma) % 4096 == 0); freelist->return_element(new_dma, 8192); }- Example - NULL pointer with alignment
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // NULL pointer - allocates new aligned block auto result = freelist->realloc_aligned(nullptr, 0, 512, 64, true); // Equivalent to alloc_aligned(512, 64, true) assert(result.hasValue()); assert(reinterpret_cast<uintptr_t>(result.value()) % 64 == 0); freelist->return_element(result.value(), 512);Note
Growth always allocates a new block with the requested alignment. The returned pointer will differ from the original when growing.
Note
The effective alignment is max(requested, freelist_default_alignment).
Warning
If the original allocation had strict alignment and you request weaker alignment on realloc, the new block may not preserve the original alignment. Specify alignment explicitly if needed.
- Parameters:
ptr – Existing pointer previously returned by alloc() or alloc_aligned(). May be nullptr, in which case this behaves like alloc_aligned().
old_bytes – Size of the existing allocation in bytes. Must be accurate.
new_bytes – Desired new size in bytes. Must be greater than 0.
alignment – Required alignment for the new allocation. If 0, uses freelist’s default alignment. Must be power of 2.
zeroed – If true and the allocation grows, the newly added tail region [old_bytes, new_bytes) is zero-initialized.
- Return values:
Expected – with void* pointer (aligned to requested alignment) on success
Expected – with ArgumentError if new_bytes == 0
Expected – with AlignmentError if alignment is not power of 2
Expected – with CapacityOverflowError if growth fails
- Returns:
Expected containing aligned pointer on success, or error
- virtual void return_element(void *ptr, size_t bytes = 0, size_t alignment = 0) override
Return an allocated block to the freelist for reuse.
Returns an allocated block to the freelist, making it available for future allocations. The freelist automatically coalesces the freed block with adjacent free blocks to reduce external fragmentation.
The method performs extensive validation:
Pointer is within freelist memory region
Valid FreeListHeader exists before the pointer
Block size and offset are sane
Block fits within freelist bounds
The freed block is inserted into the free list in address order, then coalesced with adjacent blocks:
Forward coalescing: Merges with next block if adjacent
Backward coalescing: Merges with previous block if adjacent
Coalescing prevents fragmentation by combining small free blocks into larger contiguous regions.
See also
alloc() For allocating blocks
See also
alloc_aligned() For aligned allocations
See also
reset() To free all allocations at once
See also
is_ptr() To validate pointers before freeing
- Example - Basic free and reuse
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // Allocate block auto ptr = freelist->alloc(256, false).value(); // Use the allocation memset(ptr, 0x42, 256); // Return to freelist freelist->return_element(ptr, 256); // bytes parameter is ignored // ptr is now invalid - do not use it // Next allocation may reuse the freed block auto ptr2 = freelist->alloc(256, false).value(); // ptr2 might equal ptr (block was reused)- Example - Coalescing demonstration
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Allocate three adjacent blocks auto ptr1 = freelist->alloc(256, false).value(); auto ptr2 = freelist->alloc(256, false).value(); auto ptr3 = freelist->alloc(256, false).value(); std::cout << "Free blocks: " << count_free_blocks(freelist) << std::endl; // Output: Free blocks: 1 (one large remaining block) // Free middle block freelist->return_element(ptr2, 256); // Output: Free blocks: 2 (middle block + remaining) // Free first block - coalesces with middle freelist->return_element(ptr1, 256); // Output: Free blocks: 2 (first+middle merged, remaining) // Free last block - coalesces all three freelist->return_element(ptr3, 256); // Output: Free blocks: 1 (all merged back into one large block)- Example - Safe double-free protection
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); auto ptr = freelist->alloc(128, false).value(); // Free once freelist->return_element(ptr, 128); // Accidentally free again - silently ignored (no crash) freelist->return_element(ptr, 128); // Safe, but still a bug in caller code // Best practice: null out pointer after freeing ptr = nullptr;- Example - RAII wrapper for automatic cleanup
template<typename T> class FreeListPtr { FreeListAllocator* freelist_; T* ptr_; size_t size_; public: FreeListPtr(FreeListAllocator* fl, T* p, size_t sz) : freelist_(fl), ptr_(p), size_(sz) {} ~FreeListPtr() { if (ptr_) { freelist_->return_element(ptr_, size_); } } T* get() { return ptr_; } T* operator->() { return ptr_; } }; // Usage auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); { auto result = freelist->alloc(256, false); FreeListPtr<uint8_t> managed(freelist.get(), static_cast<uint8_t*>(result.value()), 256); // Use managed.get()... } // Automatically freed here- Example - Tracking allocations for bulk free
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); std::vector<std::pair<void*, size_t>> allocations; // Make several allocations allocations.push_back({freelist->alloc(128, false).value(), 128}); allocations.push_back({freelist->alloc(256, false).value(), 256}); allocations.push_back({freelist->alloc(512, false).value(), 512}); // Use allocations... // Bulk free for (const auto& [ptr, size] : allocations) { freelist->return_element(ptr, size); }Note
The bytes and alignment parameters are ignored. FreeListAllocator retrieves size information from the block’s header.
Note
This method is silent on invalid pointers - it returns without error if validation fails. This is defensive programming to prevent crashes.
Note
Double-freeing the same pointer is detected and ignored safely.
Warning
After calling this method, ptr becomes invalid and must not be dereferenced. Any attempt to use the pointer results in undefined behavior.
Warning
Passing a pointer from a different allocator results in undefined behavior. Only return pointers allocated from this freelist.
- Parameters:
ptr – Pointer to block previously allocated from this freelist. Must have been returned by alloc(), alloc_aligned(), realloc(), or realloc_aligned(). Must not be NULL.
bytes – Size parameter (unused for freelists, for interface compatibility). FreeListAllocator stores size in the block header.
alignment – Alignment parameter (unused for freelists, for interface compatibility).
- virtual bool reset(bool trim = false) override
Reset freelist to initial empty state.
Resets the freelist to its initial pristine state, as if freshly created. All allocation state is cleared and the entire usable region is rebuilt as a single large free block.
The reset operation:
Clears usage accounting (len_ = 0, size_ = 0)
Resets high-water mark (cur_ = memory_)
Destroys free list
Creates single free block spanning entire region
This is an O(1) operation regardless of how many allocations exist or how fragmented the freelist has become. No memory is freed back to the operating system; the freelist retains its capacity.
See also
return_element() To free individual blocks
See also
remaining() To check available capacity after reset
- Example - Per-frame allocation pattern
auto freelist = cslt::FreeListAllocator::Heap(65536, 0, false).value(); while (game_running) { // Frame allocations auto vertex_buffer = freelist->alloc(8192, false); auto index_buffer = freelist->alloc(4096, false); auto uniform_buffer = freelist->alloc(256, false); // Render frame using buffers... render_frame(vertex_buffer.value(), index_buffer.value(), uniform_buffer.value()); // Fast cleanup - all frame allocations freed instantly freelist->reset(); // Ready for next frame with zero fragmentation }- Example - Request/response processing
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); void handle_request(const Request& req) { // Clear previous request's allocations freelist->reset(); // Allocate buffers for this request void* parse_buffer = freelist->alloc(4096, false).value(); void* response_buffer = freelist->alloc(8192, false).value(); // Process request... // No manual cleanup needed - next request will reset() }- Example - Temporary computation workspace
auto freelist = cslt::FreeListAllocator::Heap(32768, 0, false).value(); void complex_computation() { // Many temporary allocations std::vector<void*> temp_buffers; for (int i = 0; i < 100; ++i) { auto temp = freelist->alloc(256, false); if (temp.hasValue()) { temp_buffers.push_back(temp.value()); } } // Use temporary buffers... // Fast cleanup - O(1) instead of freeing 100 individual blocks freelist->reset(); }- Example - Testing/benchmarking reset
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // Allocate and fragment the freelist std::vector<void*> ptrs; for (int i = 0; i < 10; ++i) { auto ptr = freelist->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } // Free every other block (create fragmentation) for (size_t i = 0; i < ptrs.size(); i += 2) { freelist->return_element(ptrs[i], 128); } std::cout << "Before reset - used: " << freelist->used() << std::endl; std::cout << "Before reset - fragmented" << std::endl; // Reset to pristine state bool ok = freelist->reset(); assert(ok); std::cout << "After reset - used: " << freelist->used() << std::endl; // 0 std::cout << "After reset - capacity: " << freelist->capacity() << std::endl; std::cout << "After reset - no fragmentation" << std::endl; // All pointers in ptrs are now invalid- Example - Reusing freelist across operations
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Operation 1 { auto data = freelist->alloc(2048, false).value(); process_data(data); // Don't bother freeing individually } // Clean slate for operation 2 freelist->reset(); // Operation 2 { auto buffer = freelist->alloc(4096, false).value(); process_buffer(buffer); } // Clean slate for operation 3 freelist->reset(); // Continue reusing...Note
All outstanding allocations become invalid immediately. Any pointers obtained before reset() must not be used after reset().
Note
The freelist is immediately ready for new allocations after reset(). There is no fragmentation - the entire capacity is available as one contiguous block.
Note
No memory is released. The backing arena remains unchanged.
Warning
Invalidates ALL outstanding pointers. Ensure no code holds pointers across a reset() call.
- Parameters:
trim – Unused for freelists (for interface compatibility). Freelists do not trim or release memory back to the system.
- Returns:
true if reset succeeded, false if freelist is not initialized
- virtual bool is_ptr(void *ptr) const override
Validate whether a pointer belongs to this freelist.
Performs comprehensive validation to determine if a pointer was allocated by this freelist and is still valid. The validation checks:
Pointer is not NULL
Pointer is within the freelist’s memory region
Pointer has room for FreeListHeader before it
Valid header exists with sane block_size and offset
Reconstructed block boundaries fit within memory region
Pointer lies within the reconstructed block
This method cannot distinguish between currently allocated blocks and blocks that have been freed (returned to free list). It only validates that the pointer could have come from this freelist based on memory layout and metadata.
See also
is_ptr_sized() To additionally validate available size
See also
return_element() Uses validation internally before freeing
Note
Returns false for NULL pointers without error.
Note
Returns false for pointers from other allocators.
Note
Cannot detect if a pointer has been freed - only validates that it could be a valid freelist pointer based on structure.
Note
Useful for defensive programming and debugging, but not foolproof against all forms of pointer corruption.
- Parameters:
ptr – Pointer to validate. May be NULL.
- Returns:
true if pointer is a valid allocation from this freelist, false otherwise
- virtual bool is_ptr_sized(void *ptr, size_t bytes) const override
Validate pointer and verify it has at least the requested size.
Extends is_ptr() validation with size checking. First validates that the pointer is a plausible freelist allocation using the same checks as is_ptr(). Then additionally verifies:
Requested bytes is greater than 0
Block’s user data region is large enough (user_data_size >= bytes)
ptr + bytes does not exceed freelist memory bounds
The user data size is calculated as (block_size - offset), representing the actual space available to the user after accounting for the header and alignment padding.
This method is useful for bounds checking before writing to a buffer, ensuring that the allocation is large enough to hold the intended data.
Note
Returns false if bytes == 0 (zero-size check is invalid).
Note
Returns false if ptr fails basic is_ptr() validation.
Note
The check is conservative - it validates against the block’s actual capacity, not just the user’s original allocation request.
Warning
Does not prevent buffer overruns within the validated size. It only checks that the block is large enough, not that subsequent writes will be bounds-safe.
- Parameters:
ptr – Pointer to validate. May be NULL.
bytes – Minimum number of bytes required to be available at the pointer.
- Returns:
true if pointer is valid and has at least bytes available, false otherwise
- virtual bool stats(char *buffer, size_t buffer_size) const override
Generate diagnostic statistics report for the freelist.
Generates a human-readable, multi-line text report describing the internal state of the freelist allocator. The report includes:
Type: STATIC or DYNAMIC (inherited from backing arena)
Ownership: Whether the freelist owns its arena
Memory accounting: Used bytes, remaining bytes, capacity
Overhead: Total size including headers
Utilization: Percentage of capacity currently in use
Alignment: Base alignment for allocations
Free list: Enumeration of all free blocks with addresses and sizes
The report is written using internal
_buf_appendf()utility which safely handles buffer overflow. Output is always null-terminated as long as buffer_size >= 1.This method is useful for debugging, logging, performance analysis, and verification during development. It provides insight into fragmentation, capacity utilization, and memory layout.
See also
remaining() For just available capacity
See also
used() For just current usage
See also
capacity() For just total capacity
- Example - Basic statistics report
auto freelist = cslt::FreeListAllocator::Heap(8192, 0, false).value(); // Make some allocations auto ptr1 = freelist->alloc(512, false).value(); auto ptr2 = freelist->alloc(1024, false).value(); auto ptr3 = freelist->alloc(256, false).value(); // Free one to create fragmentation freelist->return_element(ptr2, 1024); // Generate report char buffer[2048]; if (freelist->stats(buffer, sizeof(buffer))) { std::cout << buffer << std::endl; } else { std::cerr << "Failed to generate stats" << std::endl; }- Example Output
FreeListAllocator Statistics: Type: DYNAMIC Owns arena: yes Used (accounted): 1872 bytes Remaining: 6224 bytes Capacity (usable region): 8096 bytes Total (with header/overhead): 8288 bytes Utilization: 23.1% Base alignment: 16 bytes Free block 1: 0x7f8a4c002400, 1024 bytes Free block 2: 0x7f8a4c002c00, 5200 bytes Free blocks: 2, total free bytes (raw): 6224- Example - Comparing before/after reset
auto freelist = cslt::FreeListAllocator::Heap(4096, 0, false).value(); // Create fragmentation std::vector<void*> ptrs; for (int i = 0; i < 5; ++i) { auto ptr = freelist->alloc(256, false); if (ptr.hasValue()) ptrs.push_back(ptr.value()); } // Free alternating blocks for (size_t i = 0; i < ptrs.size(); i += 2) { freelist->return_element(ptrs[i], 256); } char buffer[2048]; // Before reset std::cout << "=== BEFORE RESET ===" << std::endl; freelist->stats(buffer, sizeof(buffer)); std::cout << buffer << std::endl; // Reset freelist->reset(); // After reset std::cout << "=== AFTER RESET ===" << std::endl; freelist->stats(buffer, sizeof(buffer)); std::cout << buffer << std::endl;- Example Output - Before/After Reset
=== BEFORE RESET === FreeListAllocator Statistics: Type: DYNAMIC Owns arena: yes Used (accounted): 768 bytes Remaining: 3328 bytes Capacity (usable region): 4096 bytes Total (with header/overhead): 4288 bytes Utilization: 18.8% Base alignment: 16 bytes Free block 1: 0x7f8a4c002000, 256 bytes Free block 2: 0x7f8a4c002200, 256 bytes Free block 3: 0x7f8a4c002400, 256 bytes Free block 4: 0x7f8a4c002a00, 2560 bytes Free blocks: 4, total free bytes (raw): 3328 === AFTER RESET === FreeListAllocator Statistics: Type: DYNAMIC Owns arena: yes Used (accounted): 0 bytes Remaining: 4096 bytes Capacity (usable region): 4096 bytes Total (with header/overhead): 4288 bytes Utilization: 0.0% Base alignment: 16 bytes Free block 1: 0x7f8a4c002000, 4096 bytes Free blocks: 1, total free bytes (raw): 4096- Example - Logging to file
auto freelist = cslt::FreeListAllocator::Heap(16384, 0, false).value(); // Perform operations... // Log statistics to file char buffer[4096]; if (freelist->stats(buffer, sizeof(buffer))) { std::ofstream log("freelist_stats.txt"); log << "Freelist Statistics at " << current_timestamp() << std::endl; log << buffer << std::endl; log.close(); }Note
If the buffer is too small, the report will be truncated but still null-terminated and safe to use.
Note
This method does not allocate memory - all output uses the provided buffer.
Warning
Large freelists with many free blocks may produce output exceeding typical buffer sizes. Consider using a 2KB or larger buffer for detailed reports.
- Parameters:
buffer – Destination character buffer for the report. Must not be NULL.
buffer_size – Size of buffer in bytes. Must be greater than 0.
- Return values:
true – Report generated successfully (may be truncated if buffer too small)
false – Invalid parameters (buffer is NULL or buffer_size is 0)
- Returns:
true if report was successfully generated, false on error
- virtual size_t remaining() const noexcept
Get the number of bytes available for new allocations.
Returns the number of bytes currently available for new allocations, calculated as (capacity - used). This represents the logical free space, accounting for all overhead including headers and alignment padding that has been consumed by existing allocations.
Note that the actual usable space for a new allocation may be less than this value due to:
Fragmentation (free space split across multiple small blocks)
Alignment requirements for the new allocation
Per-allocation header overhead (FreeListHeader)
See also
capacity() For total usable capacity
See also
used() For currently consumed bytes
Note
This is a logical capacity measure, not a guarantee that an allocation of this size will succeed. Use is_ptr_sized() to validate actual allocation feasibility.
Note
The value equals capacity() immediately after construction or reset().
- Returns:
Number of bytes remaining for allocation
- size_t used() const
Get the number of bytes currently consumed by allocations.
Returns the total number of bytes consumed by current allocations, including all internal overhead such as:
FreeListHeader metadata for each allocated block
Alignment padding between headers and user data
Full block consumption when remainders are too small to split
This is the internal accounting value (len_) which tracks the total size charged for all outstanding allocations. It increases with each alloc() call and decreases with each return_element() call.
The value may be significantly larger than the sum of user-requested allocation sizes due to internal overhead. For example, a 256-byte allocation might consume 280+ bytes when accounting for header and alignment padding.
See also
remaining() For available bytes
See also
capacity() For total capacity
See also
stats() For detailed usage breakdown
Note
This is the total block size consumed, not just user-visible bytes. It includes all metadata and padding.
Note
Returns 0 immediately after construction or reset().
Note
The relationship: used() + remaining() == capacity() always holds.
- Returns:
Number of bytes currently in use (including internal overhead)
- bool owns_arena() const
Check whether the freelist owns its backing arena.
Returns the ownership status of the freelist’s backing arena, which determines cleanup behavior when the freelist is destroyed.
Ownership by factory method:
Heap(): Returns true - freelist owns the arena (DYNAMIC)
WithArena(): Returns false - arena is borrowed
Stack(): Returns true - freelist owns the arena object (STATIC)
When owns_arena() returns true, the FreeListDeleter will destroy the arena when the freelist is destroyed:
For DYNAMIC arenas (Heap): Arena is deleted
For STATIC arenas (Stack): Arena destructor called, buffer not freed
When owns_arena() returns false (WithArena), the arena outlives the freelist and remains valid for use by other allocators.
See also
Heap() Creates freelist with owned arena
See also
WithArena() Creates freelist with borrowed arena
See also
Stack() Creates freelist with owned arena object over user buffer
See also
FreeListDeleter For cleanup behavior based on ownership
Note
This indicates ownership of the arena OBJECT, not necessarily the underlying memory buffer. Stack() freelists own their arena object but not the user-provided buffer.
Note
This value is set at construction time and never changes.
- Returns:
true if the freelist owns the arena, false if arena is borrowed
- inline virtual void *save() const override
Save allocator state (not supported for freelists)
This method is implemented to satisfy the Allocator base class interface contract but is not supported for FreeListAllocator.
Unlike ArenaAllocator and PoolAllocator which support checkpointing, FreeListAllocator cannot provide meaningful checkpoint/restore functionality because:
Free blocks are managed via a linked list with pointers
Allocation metadata is interleaved with user data
Restoring would require tracking all allocation headers
No clear way to invalidate user pointers to restored allocations
For bulk cleanup of all allocations, use reset() instead, which returns the freelist to its initial empty state.
See also
reset() To clear all allocations and return to initial state
Note
This is a no-op implementation. Always returns nullptr.
Note
If checkpoint/restore functionality is needed, consider using ArenaAllocator or PoolAllocator instead.
- Returns:
Always returns nullptr
- inline virtual bool restore(void *checkpoint) override
Restore allocator state (not supported for freelists)
This method is implemented to satisfy the Allocator base class interface contract but is not supported for FreeListAllocator.
FreeListAllocator cannot support checkpoint/restore semantics because:
The free list structure uses embedded pointers that would be invalidated
Allocation headers contain metadata that cannot be simply discarded
User pointers to “restored away” allocations would become dangling
No efficient way to track which allocations to invalidate
For resetting the allocator state, use reset() which clears all allocations and returns the freelist to pristine condition.
See also
reset() To clear all allocations and start fresh
Note
This is a no-op implementation. The checkpoint parameter is ignored and the method always returns false.
Note
Attempting to use checkpoints from other allocator types will fail safely (returns false) but should be avoided.
- Parameters:
checkpoint – Checkpoint pointer (ignored, must be from save())
- Returns:
Always returns false
Public Static Functions
- static Expected<UniquePtr<FreeListAllocator, FreeListDeleter>> Heap(size_t bytes, size_t alignment = 0, bool resize = false)
Create a dynamically-backed freelist allocator (PREFERRED for heap use)
Creates a freelist with its own dedicated heap-allocated arena. The freelist owns the arena and will destroy it on cleanup via FreeListDeleter. All memory managed by the freelist is carved from this owned arena.
The actual usable capacity may exceed the requested bytes, depending on how the underlying arena rounds allocations and alignment padding requirements.
See also
WithArena() For sharing an arena between multiple allocators
See also
Stack() For stack/embedded scenarios with user-provided buffers
See also
FreeListDeleter For cleanup behavior
- Example - Basic heap freelist
#include "FreeListAllocator.hpp" // Create 8KB freelist auto result = cslt::FreeListAllocator::Heap(8192, 0, false); if (!result.hasValue()) { // Handle error std::cerr << result.error().what() << std::endl; return; } auto freelist = cslt::move(result.value()); // Allocate variable-sized blocks auto ptr1 = freelist->alloc(256, false); auto ptr2 = freelist->alloc(512, true); // Zero-initialized auto ptr3 = freelist->alloc(128, false); // Use allocations... // Free blocks (will be coalesced if adjacent) freelist->return_element(ptr1.value(), 256); freelist->return_element(ptr3.value(), 128); // Freelist and arena automatically destroyed when freelist goes out of scope- Example - Custom alignment
// Create freelist with 64-byte alignment auto freelist = cslt::FreeListAllocator::Heap(4096, 64, false).value(); // All allocations will be at least 64-byte aligned auto ptr = freelist->alloc(256, false); assert(reinterpret_cast<uintptr_t>(ptr.value()) % 64 == 0);Note
The freelist owns the arena. Call reset() on the UniquePtr to release all associated memory.
Note
Requires ARENA_ENABLE_DYNAMIC to be enabled at compile time.
Warning
All pointers obtained from this freelist become invalid once the freelist is destroyed.
- Parameters:
bytes – Minimum usable payload size (excluding metadata). If 0, defaults to a reasonable minimum (4096 bytes). Must be at least sizeof(FreeBlock).
alignment – Desired alignment for allocations (0 = default = alignof(max_align_t)). Must be power of 2. The effective alignment is always at least alignof(max_align_t).
resize – Whether the underlying arena is permitted to grow. Currently, the freelist itself remains fixed-size after construction.
- Return values:
Expected – with UniquePtr<FreeListAllocator> on success
Expected – with ArgumentError if bytes < minimum size
Expected – with AlignmentError if alignment is not power of 2
Expected – with LengthOverflowError if size calculations overflow
Expected – with MemoryError if arena allocation fails
- Returns:
Expected containing UniquePtr to freelist on success, or error
- static Expected<UniquePtr<FreeListAllocator, FreeListDeleter>> WithArena(ArenaAllocator &arena, size_t bytes, size_t alignment = 0)
Create a freelist using a borrowed arena (PREFERRED for shared arena)
Creates a freelist within an existing arena’s memory space. The freelist does NOT own the arena - the caller or another component is responsible for the arena’s lifetime. Multiple allocators can share the same arena for efficient memory use.
The freelist inherits its memory type (STATIC/DYNAMIC) from the parent arena. When the freelist is destroyed, the arena remains valid and usable by other allocators.
See also
Heap() For standalone freelists with owned arenas
See also
Stack() For stack-based freelists
- Example - Multiple freelists sharing one arena
#include "FreeListAllocator.hpp" #include "ArenaAllocator.hpp" // Create parent arena auto arena = cslt::ArenaAllocator::Heap(64 * 1024).value(); // Create multiple freelists sharing the arena auto freelist1 = cslt::FreeListAllocator::WithArena(*arena, 8192, 0).value(); auto freelist2 = cslt::FreeListAllocator::WithArena(*arena, 4096, 0).value(); auto freelist3 = cslt::FreeListAllocator::WithArena(*arena, 2048, 0).value(); // All freelists allocate from the same 64KB arena auto ptr1 = freelist1->alloc(512, false); auto ptr2 = freelist2->alloc(256, false); auto ptr3 = freelist3->alloc(128, false); // Freelists can be destroyed independently freelist1.reset(); freelist2.reset(); // Arena still valid for freelist3 auto ptr4 = freelist3->alloc(64, false); // Arena destroyed last (after all freelists)- Example - Mixing allocator types
auto arena = cslt::ArenaAllocator::Heap(128 * 1024).value(); // Mix different allocator types on same arena auto freelist = cslt::FreeListAllocator::WithArena(*arena, 16384, 0).value(); auto pool = cslt::PoolAllocator::WithArena(*arena, 256, 64, 0, true, true).value(); // Both share the same underlying memory efficiently auto var_sized = freelist->alloc(1024, false); auto fixed_sized = pool->alloc_pool(false);Note
Arena must remain valid for the freelist’s entire lifetime.
Note
The freelist does NOT own the arena. Destroying the freelist does not affect the arena.
Warning
Destroying the arena while freelists still reference it results in undefined behavior.
- Parameters:
arena – Reference to existing arena (must outlive freelist). The arena is borrowed, not owned. Multiple freelists can share the same arena.
bytes – Usable payload size to allocate from arena (excluding metadata). Must be at least sizeof(FreeBlock).
alignment – Desired alignment for allocations (0 = default = alignof(max_align_t)). Must be power of 2.
- Return values:
Expected – with UniquePtr<FreeListAllocator> on success
Expected – with ArgumentError if bytes < minimum size
Expected – with AlignmentError if alignment is not power of 2
Expected – with LengthOverflowError if size calculations overflow
Expected – with MemoryError if arena cannot supply requested memory
- Returns:
Expected containing UniquePtr to freelist on success, or error
- static Expected<UniquePtr<FreeListAllocator, FreeListDeleter>> Stack(void *buffer, size_t buffer_bytes, size_t alignment = 0)
Create a freelist over a user-supplied buffer (PREFERRED for embedded)
Constructs a freelist entirely within a caller-provided memory buffer. No heap allocation is performed. The freelist does NOT take ownership of the buffer; the caller is responsible for ensuring that the buffer remains valid for the full lifetime of the freelist.
Internally, this creates a non-owning STATIC arena over the supplied buffer and then carves the freelist allocator from that arena. The freelist owns the arena object but not the underlying buffer.
See also
Heap() For heap-allocated freelists
See also
WithArena() For sharing arenas
- Example - Stack-based freelist
#include "FreeListAllocator.hpp" void process_data() { uint8_t buffer[4096]; // Stack-allocated buffer auto result = cslt::FreeListAllocator::Stack(buffer, sizeof(buffer), 0); if (!result.hasValue()) { // Handle error return; } auto freelist = cslt::move(result.value()); // Allocate from stack buffer auto ptr1 = freelist->alloc(256, false); auto ptr2 = freelist->alloc(512, false); // Use allocations... freelist->return_element(ptr1.value(), 256); // Freelist automatically cleaned up at scope exit } // Buffer still valid after freelist destruction- Example - Embedded system with static buffer
// Global or static buffer static uint8_t g_memory_pool[8192]; void init_allocator() { auto freelist = cslt::FreeListAllocator::Stack( g_memory_pool, sizeof(g_memory_pool), 16 // 16-byte alignment ).value(); // Use freelist for temporary allocations void* temp = freelist->alloc(128, true); // ... process ... freelist->return_element(temp, 128); // Reset for reuse freelist->reset(); }- Example - Heap-allocated buffer (user controls lifetime)
uint8_t* buffer = new uint8_t[16384]; { auto freelist = cslt::FreeListAllocator::Stack(buffer, 16384, 0).value(); // Use freelist... auto ptr = freelist->alloc(1024, false); } // Freelist destroyed, but buffer remains valid // Buffer can be reused or freed delete[] buffer;Note
The freelist does NOT own the buffer. Destroying or resetting the freelist does not free the buffer.
Note
Well suited for embedded systems, scratch allocators, or environments where dynamic allocation is restricted.
Warning
Buffer must outlive the freelist. Destroying the buffer before the freelist results in undefined behavior.
- Parameters:
buffer – Pointer to user-owned memory buffer. Must not be NULL. The buffer must remain valid for the freelist’s entire lifetime.
buffer_bytes – Total size of buffer in bytes. Must be large enough to contain FreeListAllocator header, arena header, and at least one FreeBlock.
alignment – Required alignment for allocations (0 = default = alignof(max_align_t)). Must be power of 2.
- Return values:
Expected – with UniquePtr<FreeListAllocator> on success
Expected – with ArgumentError if buffer is NULL or buffer_bytes == 0
Expected – with ArgumentError if buffer too small for structures
Expected – with AlignmentError if alignment is not power of 2
Expected – with MemoryError if arena initialization fails
- Returns:
Expected containing UniquePtr to freelist on success, or error
FreeListDeleter
An FreeListAllocator class can have it memory manually freed or be passed
between scopes. However, if the user does not manually free the memory, it
will be automatically freed after it leaves its originating scope.
The FreeListDeleter struct is a data structure used to destroy Arena memory
after it goes out of scope. While this is a publically available struct, the
struct is automatically invoked via a UniquePtr when an FreeListAllocator
is initialized, and the user does not need to interact with it.
- struct FreeListDeleter
Custom deleter for FreeListAllocator unique pointers.
This deleter properly cleans up FreeListAllocator instances created by factory methods (Heap, WithArena, Stack). It handles the complex ownership relationships between freelists and their backing arenas:
Arena ownership patterns:
Heap(): FreeList owns arena → ArenaDeleter frees arena and memory
WithArena(): FreeList borrows arena → Arena not deleted (caller owns it)
Stack(): FreeList owns arena object → ArenaDeleter destructs arena (but doesn’t free buffer, user owns it)
See also
See also
See also
See also
Public Functions
- inline void operator()(FreeListAllocator *freelist) const noexcept
Custom deleter for FreeListAllocator UniquePtr cleanup.
This custom deleter is invoked when a UniquePtr<FreeListAllocator> goes out of scope or is explicitly reset. It handles proper cleanup of the freelist and its backing arena based on ownership semantics.
Cleanup sequence:
Extract ownership information (owns_arena, arena, mem_type)
Call ~FreeListAllocator() destructor
Conditionally clean up arena based on ownership and type:
Heap() freelists (owns_arena=true, mem_type=DYNAMIC):
Calls ArenaDeleter{}(arena)
Arena and all memory is freed back to heap
WithArena() freelists (owns_arena=false):
No arena cleanup performed
Arena remains valid for other allocators
Stack() freelists (owns_arena=true, mem_type=STATIC):
Calls arena->~ArenaAllocator() destructor
Does NOT free buffer (user owns it)
Arena object destroyed, buffer remains valid
This deleter ensures memory is properly released for heap-allocated arenas while preserving user-owned buffers for stack-based freelists and borrowed arenas for shared freelists.
See also
Heap() Creates freelist with owned DYNAMIC arena (arena will be freed)
See also
WithArena() Creates freelist with borrowed arena (arena not touched)
See also
Stack() Creates freelist with owned STATIC arena (destructor only)
See also
~FreeListAllocator() Freelist destructor called before arena cleanup
Note
This is a noexcept function - no exceptions are thrown during cleanup.
Note
Null pointer is safely handled (early return, no crash).
Note
The freelist destructor is always called before arena cleanup to ensure proper cleanup ordering.
Note
Users should never call this directly - it is automatically invoked by UniquePtr when the freelist goes out of scope.
- Parameters:
freelist – Pointer to FreeListAllocator to delete. May be nullptr.
Buddy Overview
- class BuddyAllocator : public cslt::Allocator
Binary buddy memory allocator with power-of-two block management.
BuddyAllocator implements the binary buddy memory allocation algorithm, which organizes memory into power-of-two sized blocks. When a block is freed, it attempts to coalesce (merge) with its “buddy” block to form larger free blocks, reducing fragmentation.
All allocations are rounded up to the nearest power-of-two size (including a 16-byte header). Memory is obtained directly from the OS via mmap() (POSIX) or VirtualAlloc() (Windows). The pool size is fixed at creation time.
Two blocks are “buddies” if they have the same size, are adjacent in memory, and their combined offset satisfies: offset XOR (2^order). When both buddies are free, they merge into a larger block, potentially triggering recursive coalescing up the size hierarchy.
Performance: O(log n) for allocation, deallocation, and coalescing, where n is the number of size levels. Memory overhead is ~16 bytes per allocation for the header. This allocator is implemented on the heap. In order to drive compliance with MISRA standards, this class can be ommitted from compilation with the
STATIC ONLYflag.See also
Heap() Primary factory method
See also
alloc() Basic allocation
See also
alloc_aligned() Aligned allocation
See also
realloc() Resize allocation
See also
return_element() Free and coalesce
See also
reset() Bulk cleanup
See also
stats() Diagnostics
- Basic Usage:
// Create a 64KB buddy allocator with 64-byte minimum blocks auto buddy = BuddyAllocator::Heap(65536, 64, 0).value(); // Allocate 256 bytes auto ptr = buddy->alloc(256, false).value(); // Use memory memset(ptr, 0, 256); // Free (automatic coalescing) buddy->return_element(ptr); // Allocator destroyed automatically when buddy goes out of scope- Aligned Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Allocate 512 bytes with 256-byte alignment, zero-initialized auto ptr = buddy->alloc_aligned(512, 256, true).value(); // Verify alignment assert(reinterpret_cast<uintptr_t>(ptr) % 256 == 0); // Grow allocation auto new_ptr = buddy->realloc_aligned(ptr, 512, 1024, 256, false); if (new_ptr.hasValue()) { buddy->return_element(new_ptr.value()); }- Monitoring:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Make allocations auto p1 = buddy->alloc(128, false).value(); auto p2 = buddy->alloc(256, false).value(); // Query state size_t used = buddy->size(); // Currently allocated size_t free = buddy->remaining(); // Available size_t max = buddy->largest_block(); // Largest contiguous // Detect fragmentation if (free > max) { std::cout << "Fragmented: " << (free - max) << " bytes\n"; } // Generate report char buffer[2048]; buddy->stats(buffer, sizeof(buffer)); std::cout << buffer; buddy->return_element(p1); buddy->return_element(p2);- Coalescing:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); // Allocate adjacent blocks auto p1 = buddy->alloc(128, false).value(); auto p2 = buddy->alloc(128, false).value(); auto p3 = buddy->alloc(128, false).value(); // Free in order - blocks coalesce automatically buddy->return_element(p1); buddy->return_element(p2); buddy->return_element(p3); // Largest block increased due to coalescingNote
BuddyAllocator is not thread-safe. External synchronization required.
Note
Pool size is fixed at creation - use reset() to clear or recreate for a different size.
Note
alloc() does not guarantee custom alignment - use alloc_aligned() for specific alignment requirements.
Warning
Do not mix pointers from different allocators.
Warning
All allocations become invalid when BuddyAllocator is destroyed.
Public Functions
- ~BuddyAllocator() noexcept override
Destructor for BuddyAllocator.
The destructor is intentionally minimal and does NOT free the OS-backed memory pool or free-lists array. Cleanup is handled by the custom BuddyDeleter when the UniquePtr goes out of scope.
BuddyAllocator instances are always managed via UniquePtr with BuddyDeleter, which performs the actual resource cleanup in this order:
Free OS-backed memory pool (via os_free)
Free free-lists array (via delete[])
Call this destructor
Free BuddyAllocator structure (via ::operator delete)
See also
BuddyDeleter Custom deleter that performs resource cleanup
See also
Heap() Factory method that creates the UniquePtr with BuddyDeleter
Note
Users never call this destructor directly. The BuddyDeleter handles all cleanup automatically when the UniquePtr is destroyed or reset.
Note
All outstanding allocations become invalid when the BuddyAllocator is destroyed. Accessing freed pointers results in undefined behavior.
- virtual Expected<void*> alloc(size_t bytes, bool zeroed) override
Allocate memory from the buddy allocator.
Allocates memory using the buddy allocation algorithm. The actual block size will be rounded up to the nearest power of 2 that can accommodate both the requested bytes and the internal 16-byte BuddyHeader.
Allocation Process:
Calculate total size needed: bytes + sizeof(BuddyHeader)
Round to next power of 2 (minimum: min_block_size)
Find free block of appropriate size (or larger)
Split larger blocks if necessary (recursive division by 2)
Place header at block start, return pointer after header
Optionally zero-initialize the user portion
Block Sizing Example:
Request 100 bytes → 100 + 16 (header) = 116 → rounds to 128 bytes
Request 256 bytes → 256 + 16 (header) = 272 → rounds to 512 bytes
Request 1000 bytes → 1000 + 16 (header) = 1016 → rounds to 1024 bytes
Alignment: The returned pointer has natural alignment based on block position in the pool. It is NOT guaranteed to match base_align from Heap(). Use alloc_aligned() for specific alignment requirements.
Internal Fragmentation: Power-of-2 rounding creates internal fragmentation. A 100-byte request wastes ~12 bytes within the 128-byte block (after accounting for header).
See also
alloc_aligned() Allocation with specific alignment
See also
realloc() Resize existing allocation
See also
return_element() Free allocated memory
See also
largest_block() Check maximum allocatable size
- Basic Example:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Allocate 256 bytes (will use 512-byte block: 256 + 16 header → rounds to 512) auto ptr_result = buddy->alloc(256, false); if (ptr_result.hasValue()) { void* ptr = ptr_result.value(); // Use memory memset(ptr, 42, 256); // Free when done buddy->return_element(ptr); } else { std::cerr << "Allocation failed: " << ptr_result.error().message() << "\n"; }- Zero-Initialization Example:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); // Allocate 512 bytes, zero-initialized auto ptr = buddy->alloc(512, true).value(); // All bytes are guaranteed to be zero uint8_t* data = static_cast<uint8_t*>(ptr); assert(data[0] == 0); assert(data[511] == 0); buddy->return_element(ptr);- Multiple Allocations:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); std::vector<void*> ptrs; // Allocate until exhaustion for (int i = 0; i < 100; ++i) { auto ptr = buddy->alloc(128, false); if (!ptr.hasValue()) { break; // Pool exhausted } ptrs.push_back(ptr.value()); } std::cout << "Allocated " << ptrs.size() << " blocks\n"; // Free all for (void* ptr : ptrs) { buddy->return_element(ptr); }Note
The returned pointer is NOT the start of the block. The block starts 16 bytes earlier with the BuddyHeader.
Note
The actual usable space may be larger than requested due to power-of-2 rounding, but you should only use the requested number of bytes.
- Parameters:
bytes – Number of bytes to allocate (must be > 0)
zeroed – If true, zero-initialize the allocated memory
- Returns:
Expected containing pointer to allocated memory on success, or error on failure
- Throws:
ArgumentError – If bytes is zero
CapacityOverflowError – If bytes + header causes arithmetic overflow
MemoryError – If no free blocks available or request exceeds pool capacity
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed) override
Allocate aligned memory from the buddy allocator.
Allocates memory with a specific alignment guarantee. The allocation process is similar to alloc(), but allocates a larger block to ensure the user pointer can be positioned at the requested alignment.
Allocation Process:
Normalize alignment to power of 2 (e.g., 48 → 64)
Calculate total: bytes + header + (alignment - 1) for worst-case padding
Round to next power of 2 (minimum: min_block_size)
Find and allocate block (with splitting if needed)
Find aligned position within block
Place header immediately before aligned position
Return aligned pointer
Alignment Guarantee: The returned pointer is GUARANTEED to satisfy: reinterpret_cast<uintptr_t>(ptr) % alignment == 0
Block Sizing with Alignment:
Request 256 bytes, 64-byte align → 256 + 16 + 63 = 335 → 512 bytes
Request 512 bytes, 256-byte align → 512 + 16 + 255 = 783 → 1024 bytes
Overhead: Aligned allocations use more memory than alloc() due to:
Larger block size to accommodate alignment padding
Potential unused space before and after user data
See also
alloc() Basic allocation without alignment guarantee
See also
realloc_aligned() Resize with alignment preserved
See also
return_element() Free allocated memory
- Basic Alignment Example:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); // Allocate 512 bytes with 256-byte alignment auto ptr_result = buddy->alloc_aligned(512, 256, false); if (ptr_result.hasValue()) { void* ptr = ptr_result.value(); // Verify alignment uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % 256 == 0); // Use memory memcpy(ptr, data, 512); buddy->return_element(ptr); }- SIMD/Cache-Line Alignment:
auto buddy = BuddyAllocator::Heap(32768, 64, 0).value(); // Allocate for SIMD operations (32-byte alignment) auto simd_ptr = buddy->alloc_aligned(1024, 32, true).value(); // Safe for AVX operations __m256* vec = reinterpret_cast<__m256*>(simd_ptr); // ... SIMD code ... // Allocate for cache-line alignment (64 bytes) auto cache_ptr = buddy->alloc_aligned(2048, 64, false).value(); buddy->return_element(simd_ptr); buddy->return_element(cache_ptr);- Page Alignment Example:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Allocate page-aligned memory (4096 bytes) auto page_ptr = buddy->alloc_aligned(8192, 4096, false); if (page_ptr.hasValue()) { void* ptr = page_ptr.value(); // Guaranteed page-aligned assert(reinterpret_cast<uintptr_t>(ptr) % 4096 == 0); // Useful for memory mapping, DMA, etc. buddy->return_element(ptr); }- Non-Power-of-2 Alignment:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Request 48-byte alignment (not power of 2) auto ptr = buddy->alloc_aligned(256, 48, false).value(); // Actual alignment will be 64 (next power of 2) uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % 64 == 0); buddy->return_element(ptr);- Zero-Initialization with Alignment:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); // Allocate aligned and zeroed auto ptr = buddy->alloc_aligned(1024, 128, true).value(); // Both alignment AND zero-initialization guaranteed assert(reinterpret_cast<uintptr_t>(ptr) % 128 == 0); uint8_t* data = static_cast<uint8_t*>(ptr); for (size_t i = 0; i < 1024; ++i) { assert(data[i] == 0); } buddy->return_element(ptr);Note
alignment=0 uses the platform’s natural alignment (alignof(max_align_t))
Note
Non-power-of-2 alignments are automatically rounded up. For example, requesting 48-byte alignment will be rounded to 64 bytes.
Note
The header is placed immediately before the aligned user pointer, NOT at the block start (unlike alloc()).
- Parameters:
bytes – Number of bytes to allocate (must be > 0)
alignment – Required alignment in bytes (0 = alignof(max_align_t), will be rounded up to power of 2)
zeroed – If true, zero-initialize the allocated memory
- Returns:
Expected containing pointer to aligned memory on success, or error on failure
- Throws:
ArgumentError – If bytes is zero
AlignmentError – If alignment is invalid after normalization
CapacityOverflowError – If bytes + header + alignment causes overflow
MemoryError – If no free blocks available, insufficient space for alignment, or request exceeds pool capacity
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed) override
Resize an existing allocation.
Resizes an existing allocation to a new size. The behavior depends on whether the allocation is growing, shrinking, or being created/freed.
Special Cases:
realloc(nullptr, 0, n) → Equivalent to alloc(n)
realloc(ptr, old, 0) → Frees ptr, returns nullptr (success)
realloc(ptr, 0, n) → Error (old_bytes must be non-zero with valid ptr)
Shrinking (new_bytes <= current block capacity):
Returns same pointer (no reallocation)
Block is NOT split or resized (buddy system doesn’t support shrinking)
If zeroed=true and new_bytes > old_bytes, zeros the extra space
Very fast (O(1)) - no memory movement
Growing (new_bytes > current block capacity):
Allocates new larger block via alloc()
Copies min(old_bytes, usable_old) bytes to new block
Frees old block (triggers coalescing)
Returns new pointer (old pointer becomes invalid)
If new allocation fails, old pointer remains valid
Block Capacity: The usable capacity of a block is determined by its order (power-of-2 size) minus the header. For example, a block with order=8 (256 bytes) has 240 bytes usable (256 - 16 byte header).
Data Preservation: When growing, all data from the old allocation is copied to the new one. The copy size is min(old_bytes, actual_usable_capacity) to prevent reading beyond the old allocation.
See also
alloc() Initial allocation
See also
realloc_aligned() Realloc with alignment preserved
See also
return_element() Free memory
- Growing Example:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Initial allocation: 128 bytes auto ptr = buddy->alloc(128, false).value(); memset(ptr, 42, 128); // Grow to 512 bytes auto new_ptr_result = buddy->realloc(ptr, 128, 512, false); if (new_ptr_result.hasValue()) { void* new_ptr = new_ptr_result.value(); // Data preserved (first 128 bytes still contain 42) uint8_t* data = static_cast<uint8_t*>(new_ptr); assert(data[0] == 42); assert(data[127] == 42); // New space available (bytes 128-511) memset(data + 128, 0, 384); buddy->return_element(new_ptr); } else { // Old pointer still valid on failure buddy->return_element(ptr); }- Shrinking Example:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); // Allocate 512 bytes (gets a power-of-2 block) auto ptr = buddy->alloc(512, false).value(); // Shrink to 256 bytes auto result = buddy->realloc(ptr, 512, 256, false); // Returns same pointer (no reallocation needed) assert(result.value() == ptr); // Still using same block, just treating it as smaller buddy->return_element(result.value());- Realloc from nullptr:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); void* ptr = nullptr; // Realloc from nullptr acts as alloc auto result = buddy->realloc(ptr, 0, 256, true); if (result.hasValue()) { ptr = result.value(); // Memory is zero-initialized uint8_t* data = static_cast<uint8_t*>(ptr); assert(data[0] == 0); buddy->return_element(ptr); }- Realloc to Zero (Free):
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); auto ptr = buddy->alloc(256, false).value(); // Realloc to zero frees the memory auto result = buddy->realloc(ptr, 256, 0, false); // Returns nullptr (success), ptr is now invalid assert(result.hasValue()); assert(result.value() == nullptr);- Growth with Zero-Fill:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); auto ptr = buddy->alloc(128, false).value(); memset(ptr, 'A', 128); // Grow to 512 with zero-fill auto new_ptr = buddy->realloc(ptr, 128, 512, true).value(); uint8_t* data = static_cast<uint8_t*>(new_ptr); // Old data preserved assert(data[0] == 'A'); assert(data[127] == 'A'); // New space zeroed assert(data[128] == 0); assert(data[511] == 0); buddy->return_element(new_ptr);Note
The returned pointer may be different from the input pointer, even when shrinking. Always use the returned pointer.
Note
When growing fails, the old pointer remains valid and unchanged. This provides transactional semantics - the operation either succeeds completely or leaves the allocation in its original state.
Note
old_bytes is used to determine how much data to preserve when growing. Providing an incorrect value may result in data loss or reading invalid memory.
- Parameters:
ptr – Pointer to existing allocation (or nullptr)
old_bytes – Current size of the allocation in bytes
new_bytes – Desired new size in bytes
zeroed – If true, zero-initialize any newly allocated space (when growing)
- Returns:
Expected containing pointer to resized allocation on success, or error on failure
- Throws:
ArgumentError – If ptr is non-null but old_bytes is zero
ArgumentError – If ptr is nullptr and new_bytes is zero
CapacityOverflowError – If new_bytes causes arithmetic overflow
MemoryError – If growing and no free blocks available
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed) override
Resize an existing allocation while preserving alignment.
Resizes an existing allocation while maintaining alignment guarantees. Behaves similarly to realloc() but with additional alignment handling.
Special Cases:
realloc_aligned(nullptr, 0, n, a) → Equivalent to alloc_aligned(n, a)
realloc_aligned(ptr, old, 0, a) → Frees ptr, returns nullptr (success)
realloc_aligned(ptr, 0, n, a) → Error (old_bytes must be non-zero)
In-Place Reuse (no reallocation): Occurs when ALL of these conditions are met:
new_bytes fits in current block capacity
ptr already satisfies the requested alignment Returns same pointer, very fast (O(1))
Reallocation Required: Occurs when ANY of these is true:
new_bytes exceeds current block capacity
ptr doesn’t satisfy requested alignment (alignment changed) Allocates new block with alloc_aligned(), copies data, frees old block
Alignment Changes: You can change alignment during realloc. For example, reallocating from 32-byte to 64-byte alignment will trigger a new allocation even if the size fits in the current block.
Data Preservation: When reallocating, copies min(old_bytes, actual_usable_capacity) bytes to preserve all data from the old allocation.
See also
alloc_aligned() Initial aligned allocation
See also
realloc() Realloc without alignment requirements
See also
return_element() Free memory
- Growing with Same Alignment:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); // Allocate 256 bytes with 128-byte alignment auto ptr = buddy->alloc_aligned(256, 128, false).value(); memset(ptr, 'X', 256); // Grow to 1024 bytes, keep 128-byte alignment auto new_ptr_result = buddy->realloc_aligned(ptr, 256, 1024, 128, false); if (new_ptr_result.hasValue()) { void* new_ptr = new_ptr_result.value(); // Alignment preserved assert(reinterpret_cast<uintptr_t>(new_ptr) % 128 == 0); // Data preserved uint8_t* data = static_cast<uint8_t*>(new_ptr); assert(data[0] == 'X'); buddy->return_element(new_ptr); }- In-Place Reuse:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Allocate with 64-byte alignment (gets larger block for padding) auto ptr = buddy->alloc_aligned(256, 64, false).value(); // Shrink with same alignment auto result = buddy->realloc_aligned(ptr, 256, 128, 64, false); // Same pointer returned (in-place reuse) if (result.hasValue()) { assert(result.value() == ptr); buddy->return_element(result.value()); }- Changing Alignment:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); // Start with 32-byte alignment auto ptr = buddy->alloc_aligned(512, 32, false).value(); memset(ptr, 'A', 512); // Change to 256-byte alignment (same size) auto new_ptr = buddy->realloc_aligned(ptr, 512, 512, 256, false).value(); // Different pointer (reallocation occurred due to alignment change) // But data is preserved uint8_t* data = static_cast<uint8_t*>(new_ptr); assert(data[0] == 'A'); assert(reinterpret_cast<uintptr_t>(new_ptr) % 256 == 0); buddy->return_element(new_ptr);- Realloc SIMD Buffer:
auto buddy = BuddyAllocator::Heap(32768, 64, 0).value(); // Start with AVX alignment (32 bytes) auto ptr = buddy->alloc_aligned(1024, 32, true).value(); // Process data with AVX... __m256* vec = reinterpret_cast<__m256*>(ptr); // ... SIMD operations ... // Grow buffer for AVX-512 (64-byte alignment) auto new_ptr = buddy->realloc_aligned(ptr, 1024, 2048, 64, false).value(); // Now suitable for AVX-512 __m512* vec512 = reinterpret_cast<__m512*>(new_ptr); // ... AVX-512 operations ... buddy->return_element(new_ptr);- Growth with Zero-Fill and Alignment:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); auto ptr = buddy->alloc_aligned(256, 128, false).value(); memset(ptr, 'B', 256); // Grow to 1024 bytes with zero-fill, maintain alignment auto new_ptr = buddy->realloc_aligned(ptr, 256, 1024, 128, true).value(); uint8_t* data = static_cast<uint8_t*>(new_ptr); // Old data preserved assert(data[0] == 'B'); assert(data[255] == 'B'); // New space zeroed (implementation zeros from new_bytes onward in new block) // Note: Exact zeroing behavior may vary based on block positioning // Alignment maintained assert(reinterpret_cast<uintptr_t>(new_ptr) % 128 == 0); buddy->return_element(new_ptr);- Realloc from nullptr with Alignment:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); void* ptr = nullptr; // Acts as alloc_aligned auto result = buddy->realloc_aligned(ptr, 0, 512, 256, true); if (result.hasValue()) { ptr = result.value(); // Aligned and zeroed assert(reinterpret_cast<uintptr_t>(ptr) % 256 == 0); buddy->return_element(ptr); }Note
alignment is normalized to a power of 2. Requesting 48 will be rounded to 64.
Note
The returned pointer may be different from the input pointer. Always use the returned pointer and consider the old pointer invalid.
Note
When growing fails, the old pointer remains valid and unchanged.
Note
Changing alignment (even to a less strict value) may trigger reallocation.
- Parameters:
ptr – Pointer to existing allocation (or nullptr)
old_bytes – Current size of the allocation in bytes
new_bytes – Desired new size in bytes
alignment – Required alignment in bytes (0 = alignof(max_align_t), will be rounded up to power of 2)
zeroed – If true, zero-initialize any newly allocated space (when growing)
- Returns:
Expected containing pointer to resized aligned allocation on success, or error on failure
- Throws:
ArgumentError – If ptr is non-null but old_bytes is zero
AlignmentError – If alignment is invalid after normalization
CapacityOverflowError – If new_bytes + alignment causes overflow
MemoryError – If growing and no free blocks available or insufficient space for alignment
- virtual void return_element(void *ptr, size_t bytes = 0, size_t alignment = 0) override
Free an allocated block and trigger buddy coalescing.
Frees a previously allocated block and attempts to coalesce (merge) it with its buddy block to form larger free blocks. This is the core deallocation mechanism of the buddy allocator.
Deallocation Process:
Validate pointer is not null (null is silently ignored)
Read BuddyHeader located 16 bytes before the user pointer
Validate header (order within valid range, offset within pool)
Calculate block start from header’s block_offset
Update accounting (decrease size_)
Begin coalescing process
Coalescing Algorithm: The allocator recursively merges blocks with their buddies:
Calculate buddy offset: current_offset XOR (2^current_order)
Check if buddy is in the free list at this level
If buddy is free:
Remove buddy from free list
Merge with current block (use lower address)
Increase order (double the size)
Repeat at next level
If buddy is NOT free (still allocated):
Stop coalescing
Insert current block into appropriate free list
Coalescing Example:
Pool: 1024 bytes (order 10) Free block at offset 0, order 7 (128 bytes): Buddy at offset: 0 XOR 128 = 128 If buddy at 128 is free: Merge → 256-byte block at offset 0, order 8 New buddy at: 0 XOR 256 = 256 If buddy at 256 is free: Merge → 512-byte block at offset 0, order 9 Continue until buddy not free or reach max_orderPerformance: O(log n) where n is the number of size levels, due to recursive coalescing up the buddy tree.
See also
alloc() Allocate memory
See also
alloc_aligned() Allocate aligned memory
See also
reset() Bulk deallocation of all blocks
See also
largest_block() Check largest free block after coalescing
- Basic Usage:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); auto ptr = buddy->alloc(256, false).value(); // Use memory... memset(ptr, 0, 256); // Free when done (automatic coalescing) buddy->return_element(ptr); // ptr is now invalid - do not use!- Coalescing Demonstration:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); // Allocate three adjacent blocks auto p1 = buddy->alloc(128, false).value(); auto p2 = buddy->alloc(128, false).value(); auto p3 = buddy->alloc(128, false).value(); size_t largest_before = buddy->largest_block(); // Free all three - they coalesce into larger block buddy->return_element(p1); buddy->return_element(p2); buddy->return_element(p3); size_t largest_after = buddy->largest_block(); // Largest block increased due to coalescing assert(largest_after > largest_before);- Free Order Doesn’t Matter:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Allocate four blocks auto p1 = buddy->alloc(128, false).value(); auto p2 = buddy->alloc(128, false).value(); auto p3 = buddy->alloc(128, false).value(); auto p4 = buddy->alloc(128, false).value(); // Free in random order - coalescing still works buddy->return_element(p3); buddy->return_element(p1); buddy->return_element(p4); buddy->return_element(p2); // All blocks coalesced back- NULL Pointer Handling:
auto buddy = BuddyAllocator::Heap(4096, 64, 0).value(); void* ptr = nullptr; // Safe - does nothing buddy->return_element(ptr); // Conditional freeing is safe auto maybe_ptr = buddy->alloc(256, false); if (maybe_ptr.hasValue()) { ptr = maybe_ptr.value(); // ... use ptr ... } // Safe even if allocation failed (ptr is nullptr) buddy->return_element(ptr);- Fragmentation Reduction:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Allocate many blocks (creates fragmentation) std::vector<void*> ptrs; for (int i = 0; i < 30; ++i) { auto ptr = buddy->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } // Free every other block for (size_t i = 0; i < ptrs.size(); i += 2) { buddy->return_element(ptrs[i]); } size_t fragmented = buddy->largest_block(); // Free remaining blocks - coalescing reduces fragmentation for (size_t i = 1; i < ptrs.size(); i += 2) { buddy->return_element(ptrs[i]); } size_t after_coalesce = buddy->largest_block(); // Fragmentation reduced through coalescing assert(after_coalesce > fragmented);Note
The bytes and alignment parameters are ignored by BuddyAllocator. They exist only for base class interface compatibility. The allocator determines block size from the internal header.
Note
Passing nullptr is safe and does nothing (similar to free(nullptr)).
Note
Invalid pointers (wrong allocator, already freed, corrupted header) are handled silently. The function returns without error but may not free the memory correctly.
Note
Double-free results in undefined behavior. The allocator may detect some cases via header validation but cannot catch all scenarios.
Note
After this call, ptr becomes invalid. Accessing it results in undefined behavior.
Warning
Do not free pointers from a different BuddyAllocator instance. Each allocator manages its own distinct pool.
Warning
Do not modify the memory before the returned pointer. The header region must remain intact for proper deallocation.
- Parameters:
ptr – Pointer to free (returned from alloc, alloc_aligned, realloc, or realloc_aligned)
bytes – Ignored (present for interface compatibility)
alignment – Ignored (present for interface compatibility)
- virtual bool reset(bool trim = false) override
Reset allocator to initial state, freeing all allocations.
Resets the buddy allocator to its pristine initial state by clearing all free lists and placing a single large free block spanning the entire pool at the top level.
Reset Process:
Validate allocator is initialized (base, pool_size, num_levels valid)
Clear all free lists (set all levels to nullptr)
Create initial free block at pool base
Place block in top-level free list (max_order)
Reset accounting (size_ = 0)
Effect:
All previous allocations become invalid
Memory pool returns to single large free block
All fragmentation eliminated
Allocator ready for reuse
Performance: O(n) where n is the number of free list levels, but typically very fast since it just clears an array and sets a few pointers.
Use Cases:
Bulk cleanup faster than freeing individually
Per-frame allocations in game engines
Request/response cycle allocations in servers
Temporary computation scratch space
Test cleanup between test cases
See also
return_element() Free individual allocation
See also
size() Check current usage
See also
remaining() Check available space
- Basic Reset:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); // Make many allocations for (int i = 0; i < 50; ++i) { buddy->alloc(128, false); } size_t used_before = buddy->size(); assert(used_before > 0); // Reset to initial state bool ok = buddy->reset(); assert(ok); // All allocations gone size_t used_after = buddy->size(); assert(used_after == 0); // Allocator ready for reuse auto ptr = buddy->alloc(256, false); assert(ptr.hasValue());- Per-Frame Allocations:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); while (game_running) { // Frame start - allocator is clean // Allocate temporary data for this frame auto render_data = buddy->alloc(10000, false).value(); auto physics_data = buddy->alloc(5000, false).value(); auto ai_data = buddy->alloc(8000, false).value(); // Process frame... render_frame(render_data); update_physics(physics_data); update_ai(ai_data); // Frame end - bulk cleanup buddy->reset(); // All frame allocations invalidated // Ready for next frame }- Request/Response Cycle:
auto buddy = BuddyAllocator::Heap(256 * 1024, 64, 0).value(); void handle_request(const Request& req) { // Parse request (may allocate parsing buffers) auto parsed = parse_request(buddy, req); // Process request (may allocate temporary structures) auto response = process(buddy, parsed); // Send response send_response(response); // Cleanup all request-scoped allocations buddy->reset(); // Ready for next request }- Reset vs Individual Frees:
auto buddy = BuddyAllocator::Heap(16384, 64, 0).value(); std::vector<void*> ptrs; // Allocate 500 blocks for (int i = 0; i < 500; ++i) { auto ptr = buddy->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } // Option 1: Free individually (slow - 500 operations with coalescing) // for (void* ptr : ptrs) { // buddy->return_element(ptr); // } // Option 2: Reset (fast - O(levels) operation) buddy->reset(); // reset() is MUCH faster for bulk cleanup- Test Cleanup:
class BuddyTest : public ::testing::Test { protected: UniquePtr<BuddyAllocator, BuddyDeleter> buddy; void SetUp() override { buddy = BuddyAllocator::Heap(8192, 64, 0).value(); } void TearDown() override { // Clean up between tests buddy->reset(); } }; TEST_F(BuddyTest, Test1) { auto ptr = buddy->alloc(256, false); // Test logic... // No need to manually free - TearDown resets } TEST_F(BuddyTest, Test2) { // Starts with clean allocator thanks to reset() auto ptr = buddy->alloc(512, false); // Test logic... }- Temporary Computation Space:
auto buddy = BuddyAllocator::Heap(512 * 1024, 64, 0).value(); void matrix_multiply(Matrix& result, const Matrix& a, const Matrix& b) { // Allocate temporary workspace auto temp = buddy->alloc(a.rows * b.cols * sizeof(double), true).value(); // Perform computation using temp buffer // ... // Copy result memcpy(result.data, temp, result.size); // Cleanup all temporary allocations buddy->reset(); }Note
The trim parameter is ignored by BuddyAllocator. The pool size is fixed and cannot be trimmed. The parameter exists for interface compatibility.
Note
All outstanding pointers become invalid after reset(). Accessing them results in undefined behavior.
Note
The OS-backed memory pool is NOT freed. reset() only clears the internal free list structure. To free OS memory, destroy the BuddyAllocator.
Note
reset() is much faster than individually freeing many allocations, especially when there are hundreds or thousands of active allocations.
Warning
After reset(), ALL pointers obtained from this allocator become invalid. Ensure no code will access these pointers after calling reset().
- Parameters:
trim – Ignored (present for interface compatibility)
- Returns:
true on success, false if allocator is not properly initialized
- virtual bool is_ptr(void *ptr) const override
Check whether a pointer refers to a valid allocation from this allocator.
Performs a non-throwing validation of a pointer against this allocator’s internal state. The function verifies that:
ptris non-null
ptrlies within the allocator’s managed memory poolThe internal BuddyHeader immediately preceding
ptris readableThe header encodes a valid block order for this allocator
The block offset and size are consistent with the pool boundaries
ptrlies within the bounds of the referenced allocation blockThis function is designed to be memory-safe: it first performs a range check to ensure that reading the internal header cannot cause undefined behavior or a segmentation fault, even if
ptris invalid or foreign.See also
is_ptr_sized() Pointer validation with size constraint
See also
return_element() Free memory previously allocated by this allocator
- Typical Usage:
void* p = buddy->alloc(256, false).value(); assert(buddy->is_ptr(p)); // valid assert(!buddy->is_ptr(p + 8)); // interior pointer buddy->return_element(p); assert(buddy->is_ptr(p)); // may still return true (use-after-free!)Note
This function does NOT check whether the block is currently allocated or free; it only validates that the pointer is structurally consistent with a block that could have been allocated by this allocator.
Note
Passing a pointer obtained from a different allocator instance will return false.
Note
Passing an interior pointer (i.e., not the original user pointer returned by alloc/alloc_aligned/realloc*) will return false.
Note
The function is intentionally side-effect free and does not modify allocator state.
Warning
A return value of true does not guarantee that the pointer has not already been freed or that the underlying memory has not been reused. Use-after-free remains undefined behavior.
- Parameters:
ptr – Pointer to validate
- Returns:
true if
ptris a valid user pointer returned by this BuddyAllocator, false otherwise
- virtual bool is_ptr_sized(void *ptr, size_t bytes) const override
Check whether a pointer refers to a valid allocation of at least a given size.
Extends is_ptr() by additionally verifying that the allocation referenced by
ptrhas sufficient usable capacity forbytes.Validation steps include:
All structural checks performed by is_ptr()
Determination of the allocation block size from the internal header
Verification that
bytesdoes not exceed the block’s usable payload (block size minus header)This function is memory-safe and will not dereference invalid memory, even if
ptris foreign, corrupted, or out of bounds.See also
is_ptr() Basic pointer validation
See also
alloc() Allocate memory
See also
alloc_aligned() Allocate aligned memory
- Typical Usage:
auto p = buddy->alloc(256, false).value(); assert(buddy->is_ptr_sized(p, 128)); // fits assert(buddy->is_ptr_sized(p, 256)); // fits assert(!buddy->is_ptr_sized(p, 512)); // exceeds block capacity buddy->return_element(p);Note
The
bytesparameter represents a logical size requirement and is not required to match the size originally requested during allocation.Note
As with is_ptr(), this function does NOT detect use-after-free. A pointer that was previously freed may still return true if its header remains intact and has not been reused.
Note
Passing
bytes= 0 will return true for any valid pointer.
- Parameters:
ptr – Pointer to validate
bytes – Minimum required usable size in bytes
- Returns:
true if
ptris a valid user pointer returned by this BuddyAllocator and the underlying allocation can accommodate at leastbytes, false otherwise
- virtual bool stats(char *buffer, size_t buffer_size) const override
Generate a human-readable summary of allocator state.
Writes a textual report describing the current state of the BuddyAllocator into the user-provided buffer. The report includes:
Total pool size
Minimum and maximum block sizes
Currently allocated bytes
Remaining free capacity
Largest available free block
Overall utilization percentage
Per-level free list statistics (block count and free bytes)
The output is intended for diagnostics, debugging, and monitoring. The exact formatting is implementation-defined but stable enough for human consumption.
This function performs no allocations and does not modify allocator state.
See also
remaining() Query available capacity
See also
largest_block() Query largest contiguous free block
- Typical Usage:
char buf[2048]; if (buddy->stats(buf, sizeof(buf))) { std::puts(buf); } else { std::cerr << "Failed to generate allocator stats\n"; }Note
If
bufferis null orbuffer_sizeis zero, the function returns false and no output is produced.Note
If the buffer is too small to hold the full report, the function returns false. Partial output may have been written to
buffer.Note
The statistics represent a snapshot at the time of the call and may become stale immediately in multi-threaded environments.
- Parameters:
buffer – Destination buffer to receive the formatted statistics text
buffer_size – Size of
bufferin bytes- Returns:
true if statistics were successfully written to
buffer, false if an error occurred (e.g., invalid buffer or insufficient space)
- virtual size_t remaining() const noexcept override
Query the number of bytes currently available for allocation.
Returns the difference between the total pool size and the number of bytes currently allocated. This represents the aggregate free capacity and does NOT account for fragmentation.
As a result, it is possible for remaining() to return a non-zero value even when a large allocation cannot be satisfied due to fragmentation.
This function is constant-time and does not modify allocator state.
See also
largest_block() Query largest contiguous free block
- Example:
auto used = buddy->size(); auto free = buddy->remaining(); std::cout << "Used: " << used << ", Remaining: " << free << '\n';Note
To determine the maximum single allocation that can currently succeed, use largest_block() instead.
- Returns:
Number of free bytes remaining in the allocator’s memory pool
- inline virtual void *save() const override
Save allocator state (unsupported for BuddyAllocator)
This function is part of the Allocator base-class interface but is intentionally unsupported by BuddyAllocator.
Buddy allocators manage complex free-list and coalescing state that is not trivially serializable or restorable without significant overhead. As a result, checkpointing is not provided.
Note
Calling this function always returns nullptr and has no side effects.
Note
Code that relies on save()/restore() semantics should not use BuddyAllocator.
- Returns:
Always returns nullptr
- inline virtual bool restore(void *checkpoint) override
Restore allocator state from a checkpoint (unsupported for BuddyAllocator)
This function is part of the Allocator base-class interface but is intentionally unsupported by BuddyAllocator.
Because save() does not produce a valid checkpoint, restore() always fails and performs no action.
See also
reset() Clear all allocations and return allocator to initial state
Note
The allocator state is unchanged by this call.
Note
Code that requires allocator checkpointing should use an allocator that explicitly supports save/restore semantics (e.g., arena allocators).
- Parameters:
checkpoint – Ignored
- Returns:
Always returns false
- size_t largest_block() const noexcept
Query the size of the largest contiguous free block.
Scans the allocator’s free lists from the largest block size down to the smallest and returns the size of the first non-empty free list encountered.
This value represents the maximum single allocation that can currently succeed (ignoring internal header and alignment overhead).
Unlike remaining(), which reports total free capacity, this function accounts for fragmentation. A large remaining() value does not guarantee that a large allocation can succeed if free memory is split across smaller blocks.
The function runs in O(L) time, where L is the number of size levels (log₂(pool_size / min_block_size)).
See also
remaining() Total free capacity
See also
alloc() Allocation behavior
See also
stats() Detailed free-list diagnostics
- Example:
auto buddy = BuddyAllocator::Heap(8192, 64, 0).value(); auto p1 = buddy->alloc(128, false); auto p2 = buddy->alloc(128, false); size_t largest = buddy->largest_block(); if (largest < 1024) { std::cout << "Fragmentation limits large allocations\n"; }Note
The returned size is the raw block size managed by the buddy system. The actual maximum allocatable user payload may be smaller due to internal headers and alignment requirements.
Note
The result may change immediately after any allocation or deallocation.
- Returns:
Size in bytes of the largest currently available free block, or 0 if no free blocks are available
- inline size_t min_block_size() const noexcept
Query the minimum block size managed by the allocator.
Returns the smallest block size (in bytes) that this BuddyAllocator can manage internally. All allocations are rounded up to at least this size (after accounting for internal headers and alignment).
The value is determined during allocator creation and is always a power of two.
Requests smaller than this size will still consume at least one block of this size.
See also
max_block_size() Maximum possible block size
See also
Heap() Allocator creation and normalization rules
- Example:
auto buddy = BuddyAllocator::Heap(4096, 100, 0).value(); // Requested min block size: 100 bytes // Actual min block size (rounded): 128 bytes assert(buddy->min_block_size() == 128);Note
The minimum block size may be larger than the value originally requested at creation time, due to rounding to a power of two and ensuring space for internal headers and alignment.
- Returns:
Minimum allocation block size in bytes
- inline size_t max_block_size() const noexcept
Query the maximum block size managed by the allocator.
Returns the size of the largest block that the allocator can manage, which corresponds to the full memory pool size.
This value represents the theoretical upper bound for a single allocation before considering fragmentation, headers, and alignment overhead.
The value is always a power of two and is fixed for the lifetime of the allocator.
See also
min_block_size() Minimum allocation block size
See also
largest_block() Largest currently available block
See also
remaining() Aggregate free capacity
- Example:
auto buddy = BuddyAllocator::Heap(5000, 64, 0).value(); // Pool rounded up: 5000 -> 8192 assert(buddy->max_block_size() == 8192);Note
A request of exactly max_block_size() bytes may not succeed if internal headers or alignment padding are required. Use largest_block() to determine the maximum allocation that can succeed at a given moment.
- Returns:
Maximum allocation block size in bytes
Public Static Functions
- static Expected<UniquePtr<BuddyAllocator, BuddyDeleter>> Heap(size_t pool_size, size_t min_block_size, size_t base_align = 0)
Create an OS-backed buddy allocator with specified pool and block sizes.
Creates a buddy allocator with memory obtained directly from the operating system via mmap() on POSIX systems or VirtualAlloc() on Windows.
Initialization Process:
Validates and normalizes parameters (rounds to powers of 2)
Allocates OS memory pool
Initializes free-list array (one per size level)
Places initial large free block at top level
Returns UniquePtr with BuddyDeleter for automatic cleanup
Parameter Normalization:
pool_size: Rounded up to next power of 2 (e.g., 5000 → 8192)
min_block_size: Rounded up to next power of 2 (e.g., 100 → 128)
base_align: Rounded up to next power of 2 (e.g., 48 → 64)
min_block_size adjusted to hold header + alignment if needed
Size Levels: The number of free-list levels is: log2(pool_size) - log2(min_block_size) + 1 Example: pool=4096, min=64 → log2(4096) - log2(64) + 1 = 12 - 6 + 1 = 7 levels
Memory Overhead:
OS pool: pool_size bytes
Free-lists array: num_levels * sizeof(void*)
BuddyAllocator structure: ~128 bytes
See also
BuddyDeleter Custom deleter for automatic resource cleanup
See also
min_block_size() Query minimum block size
See also
max_block_size() Query maximum block size
See also
reset() Clear all allocations
- Basic Example:
// Create 64KB allocator with 64-byte minimum blocks auto result = BuddyAllocator::Heap(65536, 64, 0); if (!result.hasValue()) { std::cerr << "Error: " << result.error().message() << "\n"; return 1; } auto buddy = cslt::move(result.value()); // Use allocator... auto ptr = buddy->alloc(256, false); // Automatic cleanup when buddy goes out of scope- Power-of-2 Rounding Example:
// Request non-power-of-2 sizes auto buddy = BuddyAllocator::Heap(5000, 100, 48).value(); // Actual sizes after rounding: // pool_size: 5000 → 8192 (next power of 2) // min_block_size: 100 → 128 (next power of 2) // base_align: 48 → 64 (next power of 2) // Verify actual sizes size_t min = buddy->min_block_size(); // 128 size_t max = buddy->max_block_size(); // 8192- Error Handling Example:
// Invalid: min_block_size > pool_size auto result1 = BuddyAllocator::Heap(1024, 4096, 0); EXPECT_FALSE(result1.hasValue()); // Invalid: zero pool size auto result2 = BuddyAllocator::Heap(0, 64, 0); EXPECT_FALSE(result2.hasValue()); // Invalid: zero min_block_size auto result3 = BuddyAllocator::Heap(4096, 0, 0); EXPECT_FALSE(result3.hasValue()); // Valid: all parameters will be normalized auto result4 = BuddyAllocator::Heap(3000, 50, 12); EXPECT_TRUE(result4.hasValue());- Large Allocator Example:
// Create 16MB allocator with 256-byte minimum blocks auto buddy = BuddyAllocator::Heap(16 * 1024 * 1024, 256, 0).value(); // Can allocate up to ~16MB (minus overhead) auto large = buddy->alloc(8 * 1024 * 1024, false); EXPECT_TRUE(large.hasValue()); buddy->return_element(large.value());Note
The returned allocator is wrapped in a UniquePtr with BuddyDeleter, which automatically frees all resources (OS memory, free-lists, structure) when the UniquePtr is destroyed.
Note
The pool size is fixed for the lifetime of the allocator. It cannot be resized. Use reset() to clear allocations or destroy and recreate for a different size.
Note
base_align affects the minimum block size calculation but does NOT guarantee that alloc() returns aligned pointers. Use alloc_aligned() for specific alignment requirements.
Warning
All parameters must be representable as size_t. Extremely large values may cause overflow errors.
- Parameters:
pool_size – Total memory pool size in bytes (will be rounded up to power of 2)
min_block_size – Minimum allocation block size in bytes (will be rounded up to power of 2)
base_align – Default alignment for allocations (0 = alignof(max_align_t), will be rounded up to power of 2)
- Returns:
Expected containing UniquePtr<BuddyAllocator, BuddyDeleter> on success, or error on failure
- Throws:
ArgumentError – If pool_size or min_block_size is zero
ArgumentError – If min_block_size > pool_size (after rounding)
AlignmentError – If base_align is invalid after normalization
MemoryError – If OS memory allocation fails
CapacityOverflowError – If arithmetic overflow occurs during initialization
BuddyDeleter
An BuddyAllocator class can have it memory manually freed or be passed
between scopes. However, if the user does not manually free the memory, it
will be automatically freed after it leaves its originating scope.
The BuddyDeleter struct is a data structure used to destroy Arena memory
after it goes out of scope. While this is a publically available struct, the
struct is automatically invoked via a UniquePtr when an BuddyAllocator
is initialized, and the user does not need to interact with it.
- struct BuddyDeleter
Public Functions
- inline void operator()(BuddyAllocator *buddy) const noexcept
Custom deleter for BuddyAllocator UniquePtr cleanup.
This custom deleter is invoked when a UniquePtr<BuddyAllocator, BuddyDeleter> goes out of scope or is explicitly reset. It performs complete and ordered cleanup of all resources owned by a BuddyAllocator instance.
BuddyAllocator always owns all of its internal resources. Unlike other allocator types, it does not support borrowed memory pools or user-provided backing storage. As a result, cleanup behavior is unconditional and uniform.
Cleanup sequence:
Release the OS-backed memory pool used for allocations (via BuddyAllocator::os_free)
Free the free-lists array allocated during initialization
Invoke the BuddyAllocator destructor
Free the BuddyAllocator object itself (via ::operator delete)
This ordering guarantees that all allocator-internal state remains valid for the duration of the destructor and that no memory leaks occur.
See also
Heap() Factory method that creates a BuddyAllocator wrapped in a UniquePtr with BuddyDeleter
See also
~BuddyAllocator() Destructor invoked as part of cleanup
Note
This deleter is noexcept and never throws exceptions during cleanup.
Note
Passing a null pointer is safe and results in an immediate no-op.
Note
All outstanding allocations become invalid once this deleter is invoked. Accessing previously allocated memory after destruction results in undefined behavior.
Note
Users must not call this function directly. It is automatically invoked by UniquePtr when the BuddyAllocator goes out of scope or is reset.
Warning
Pointers returned by this BuddyAllocator must not be freed or accessed after the allocator has been destroyed.
- Parameters:
buddy – Pointer to BuddyAllocator to delete. May be nullptr.
Slab Overview
- class SlabAllocator : public cslt::Allocator
Fixed-size object allocator using slab allocation with buddy backing.
SlabAllocator provides O(1) allocation and deallocation for fixed-size objects by pre-carving memory pages (slabs) into equally-sized slots. All slots within a slab have identical size and alignment, making it ideal for allocating many instances of the same type. This allocator is implemented on the heap. In order to drive compliance with MISRA standards, this class can be ommitted from compilation with the
STATIC ONLYflag.Key Features:
O(1) allocation and deallocation (pop/push from free list)
Zero fragmentation (all objects same size)
Configurable alignment support
Lazy page allocation (grows on demand)
Backed by BuddyAllocator for page management
Limitations:
Only supports single fixed size per allocator instance
Cannot resize objects (realloc only works for same size)
Not thread-safe
Slab must be destroyed before backing buddy allocator
Use Cases:
Allocating many instances of the same struct/class
Object pools (e.g., particle systems, network packets)
Cache-friendly data structures (same-sized objects)
Reducing fragmentation for uniform allocations
See also
BuddyAllocator Backing allocator for page management
See also
WithBuddy() Factory method for creating slab allocators
- Basic Usage:
// Create buddy allocator auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab for 256-byte objects auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate objects (all must be 256 bytes) auto obj1 = slab->alloc(256, false).value(); auto obj2 = slab->alloc(256, false).value(); // Free objects slab->return_element(obj1); slab->return_element(obj2); // Slab destroyed before buddy (correct order)- Aligned Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab with 64-byte alignment for SIMD data auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // All allocations are automatically 64-byte aligned auto obj = slab->alloc(256, false).value(); assert(reinterpret_cast<uintptr_t>(obj) % 64 == 0); slab->return_element(obj);- Bulk Operations with Reset:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Per-frame allocation pattern while (game_running) { // Allocate temporary objects for (int i = 0; i < 50; ++i) { auto obj = slab->alloc(128, false).value(); process_object(obj); // Don't free individually } // Bulk free all allocations slab->reset(); // Much faster than individual frees }- Object Pool Pattern:
struct Particle { float x, y, z; float vx, vy, vz; uint32_t color; // ... total size 64 bytes }; auto buddy = BuddyAllocator::Heap(10 * 1024 * 1024, 64, 0).value(); auto particle_pool = SlabAllocator::WithBuddy(*buddy, sizeof(Particle), alignof(Particle), 0).value(); // Allocate particle auto particle_mem = particle_pool->alloc(sizeof(Particle), true).value(); Particle* p = new (particle_mem) Particle(); // Placement new // Use particle... p->x = 10.0f; // Return to pool p->~Particle(); // Explicit destructor particle_pool->return_element(particle_mem);- Statistics and Monitoring:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate some objects for (int i = 0; i < 10; ++i) { slab->alloc(256, false); } // Check statistics std::cout << "Total blocks: " << slab->total_blocks() << "\n"; std::cout << "In use: " << slab->in_use_blocks() << "\n"; std::cout << "Free: " << slab->free_blocks() << "\n"; std::cout << "Size: " << slab->size() << " bytes\n"; // Detailed report char buffer[4096]; slab->stats(buffer, sizeof(buffer)); std::cout << buffer;Note
This class is conditionally compiled. If STATIC_ONLY is defined, SlabAllocator will not be available.
Warning
Slab must be destroyed before the backing BuddyAllocator. The slab stores a non-owning pointer to the buddy and will attempt to return pages to it during destruction.
Warning
All allocations from a slab must be exactly obj_size bytes. Attempting to allocate a different size will result in an error.
Warning
Pointers become invalid after reset() or slab destruction. Accessing freed memory results in undefined behavior.
Public Functions
- ~SlabAllocator() noexcept override
Destructor - Frees all slab pages back to buddy allocator.
Walks the linked list of allocated pages and returns each page to the backing buddy allocator. The SlabAllocator structure itself is also freed back to buddy via SlabDeleter.
All outstanding allocations become invalid after destruction.
- Correct Lifetime Management:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); { auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Use slab... } // slab destroyed here (buddy still alive) - CORRECT // buddy destroyed here- Incorrect Lifetime (Undefined Behavior):
UniquePtr<SlabAllocator, SlabDeleter> slab; { auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); } // buddy destroyed first - WRONG! // slab destructor tries to free pages to destroyed buddy - CRASH!Note
This destructor is called automatically by SlabDeleter when the UniquePtr goes out of scope or is reset.
Note
The backing BuddyAllocator must still be alive when this destructor runs. Always ensure slabs are destroyed before their buddy allocator.
Warning
All pointers obtained from this slab become invalid. Accessing them after destruction results in undefined behavior.
- virtual Expected<void*> alloc(size_t bytes, bool zeroed = false) override
Allocate a fixed-size object from the slab.
Allocates a fixed-size object by popping a slot from the free list. If the free list is empty, a new page is allocated from the backing buddy allocator (lazy growth).
Allocation Process:
Validate bytes == obj_size (strict size requirement)
If free_list empty, call grow_() to allocate a new page
Pop first slot from free_list (O(1) operation)
Optionally zero-initialize the memory
Update accounting (in_use_blocks, size)
Performance:
O(1) when free slots available (simple pointer pop)
O(log n) when growing (buddy allocator overhead for new page)
Average case: O(1) amortized over many allocations
Size Constraint: Unlike general-purpose allocators, slab allocators only support ONE fixed size. The bytes parameter MUST exactly match the obj_size specified during WithBuddy() creation.
Alignment: All slots are pre-aligned to the alignment specified at creation. This method does NOT guarantee specific alignment beyond that - use alloc_aligned() if you need to verify alignment requirements.
See also
alloc_aligned() Allocate with explicit alignment verification
See also
return_element() Free allocated memory
See also
reset() Bulk free all allocations
- Basic Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate a 256-byte object auto ptr_result = slab->alloc(256, false); if (ptr_result.hasValue()) { void* ptr = ptr_result.value(); // Use the memory... uint8_t* data = static_cast<uint8_t*>(ptr); data[0] = 42; // Free when done slab->return_element(ptr); }- Zero Initialization:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate with zero-initialization auto ptr = slab->alloc(128, true).value(); uint8_t* data = static_cast<uint8_t*>(ptr); // All bytes are guaranteed to be zero for (size_t i = 0; i < 128; ++i) { assert(data[i] == 0); } slab->return_element(ptr);- Wrong Size Rejected:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Correct size auto correct = slab->alloc(256, false); EXPECT_TRUE(correct.hasValue()); // Wrong size - too small auto too_small = slab->alloc(128, false); EXPECT_FALSE(too_small.hasValue()); // Error! // Wrong size - too large auto too_large = slab->alloc(512, false); EXPECT_FALSE(too_large.hasValue()); // Error! slab->return_element(correct.value());- Multiple Allocations (Growth):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); std::vector<void*> ptrs; // Allocate many objects for (int i = 0; i < 100; ++i) { auto ptr = slab->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } // Slab automatically grew pages as needed std::cout << "Allocated: " << ptrs.size() << " objects\n"; std::cout << "Total capacity: " << slab->total_blocks() << " slots\n"; std::cout << "Pages allocated: " << slab->total_blocks() / slab->stride() << "\n"; // Cleanup for (void* ptr : ptrs) { slab->return_element(ptr); }- Struct Allocation Pattern:
struct GameObject { float x, y, z; uint32_t id; // ... total 64 bytes }; auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto objects = SlabAllocator::WithBuddy(*buddy, sizeof(GameObject), alignof(GameObject), 0).value(); // Allocate memory for object auto mem = objects->alloc(sizeof(GameObject), true).value(); // Use placement new to construct GameObject* obj = new (mem) GameObject(); obj->x = 10.0f; obj->y = 20.0f; obj->id = 123; // Use object... // Explicit destructor obj->~GameObject(); // Return to pool objects->return_element(mem);Note
bytes must EXACTLY equal obj_size. Requesting any other size results in an ArgumentError.
Note
The first allocation from a newly created slab triggers page allocation from the buddy allocator (lazy initialization).
Note
Returned pointers remain valid until freed with return_element() or until the slab is reset() or destroyed.
- Parameters:
bytes – Size in bytes (must equal obj_size specified at creation)
zeroed – If true, zero-initialize the allocated memory
- Returns:
Expected containing pointer to allocated memory on success, or error on failure
- Throws:
ArgumentError – If bytes != obj_size
MemoryError – If grow() fails (buddy allocator exhausted or page allocation failed)
- virtual Expected<void*> alloc_aligned(size_t bytes, size_t alignment, bool zeroed = false) override
Allocate a fixed-size object with alignment verification.
Allocates a fixed-size object from the slab with explicit alignment verification. The slab pre-aligns all slots during page creation, so this method validates that the requested alignment is satisfied by the slab’s alignment.
Allocation Process:
Validate bytes == obj_size (strict size requirement)
Normalize alignment (0 → max_align_t, round to power of 2)
Verify requested_align <= slab’s align (pre-aligned slots)
Delegate to alloc() to pop from free list
All returned pointers are guaranteed aligned
Alignment Constraint: The requested alignment CANNOT exceed the slab’s alignment specified at creation. To allocate with higher alignment, create the slab with a larger alignment value in WithBuddy().
Performance: Same as alloc() - O(1) amortized. The alignment check is just a comparison, adding negligible overhead.
See also
alloc() Allocate without explicit alignment verification
See also
WithBuddy() Specify slab alignment at creation
See also
return_element() Free allocated memory
- Basic Aligned Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab with 64-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // Allocate with 64-byte alignment verification auto ptr_result = slab->alloc_aligned(256, 64, false); if (ptr_result.hasValue()) { void* ptr = ptr_result.value(); // Guaranteed to be 64-byte aligned uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % 64 == 0); slab->return_element(ptr); }- Alignment Within Slab Alignment:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab with 128-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 128, 0).value(); // Request 64-byte alignment (less than slab's 128) auto ptr = slab->alloc_aligned(256, 64, false).value(); // Satisfied (128-byte aligned is also 64-byte aligned) uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % 64 == 0); assert(addr % 128 == 0); // Also true! slab->return_element(ptr);- Alignment Exceeds Slab (Error):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab with only 64-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // Request 128-byte alignment (exceeds slab's 64) auto result = slab->alloc_aligned(256, 128, false); // Fails - slab cannot satisfy this requirement EXPECT_FALSE(result.hasValue()); EXPECT_EQ(result.error().type(), ErrorType::ALIGNMENT_ERROR); // Solution: Create slab with 128-byte alignment auto slab128 = SlabAllocator::WithBuddy(*buddy, 256, 128, 0).value(); auto ptr = slab128->alloc_aligned(256, 128, false); EXPECT_TRUE(ptr.hasValue());- SIMD Data Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab for SIMD vectors (32-byte alignment for AVX) auto slab = SlabAllocator::WithBuddy(*buddy, 256, 32, 0).value(); // Allocate aligned for SIMD operations auto ptr = slab->alloc_aligned(256, 32, true).value(); // Safe to use with SIMD instructions __m256* vec = reinterpret_cast<__m256*>(ptr); *vec = _mm256_set1_ps(1.0f); slab->return_element(ptr);- Alignment Normalization:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // Request 48-byte alignment (not power of 2) // Automatically rounded to 64 auto ptr = slab->alloc_aligned(256, 48, false).value(); // Aligned to 64 (rounded up) uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % 64 == 0); slab->return_element(ptr);- Cache-Line Aligned Allocation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab for cache-line aligned data (64 bytes) auto slab = SlabAllocator::WithBuddy(*buddy, 1024, 64, 0).value(); // Allocate multiple cache-line aligned blocks for (int i = 0; i < 10; ++i) { auto ptr = slab->alloc_aligned(1024, 64, false); ASSERT_TRUE(ptr.hasValue()); uintptr_t addr = reinterpret_cast<uintptr_t>(ptr.value()); EXPECT_EQ(addr % 64, 0) << "Not cache-line aligned"; slab->return_element(ptr.value()); }- Default Alignment (Zero):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // alignment=0 means alignof(max_align_t) (typically 16) auto ptr = slab->alloc_aligned(256, 0, false).value(); // Guaranteed at least max_align_t alignment uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); assert(addr % alignof(max_align_t) == 0); slab->return_element(ptr);Note
Requested alignment must be <= slab’s alignment. If you need higher alignment, create a new slab with WithBuddy() using the desired alignment.
Note
All slots in the slab are pre-aligned, so this method just validates the requirement is met - it doesn’t perform additional alignment.
Note
alignment is normalized to a power of 2. Requesting 48 will be rounded to 64 before checking against slab alignment.
- Parameters:
bytes – Size in bytes (must equal obj_size specified at creation)
alignment – Required alignment in bytes (0 = max_align_t, rounded to power of 2)
zeroed – If true, zero-initialize the allocated memory
- Returns:
Expected containing pointer to aligned memory on success, or error on failure
- Throws:
ArgumentError – If bytes != obj_size
AlignmentError – If alignment is invalid or exceeds slab’s alignment
MemoryError – If grow() fails (buddy allocator exhausted)
- virtual Expected<void*> realloc(void *ptr, size_t old_bytes, size_t new_bytes, bool zeroed = false) override
Realloc for slab allocator (limited - same size only)
Slab allocators only support fixed-size objects, so realloc has very limited functionality compared to general-purpose allocators. The only supported operation is “realloc to same size” which simply returns the same pointer.
Special Cases:
realloc(nullptr, 0, n) → Equivalent to alloc(n)
realloc(ptr, old, 0) → Frees ptr, returns nullptr (success)
realloc(ptr, old, new) where old == new == obj_size → Returns same ptr
realloc(ptr, old, new) where old != new → ERROR (cannot resize)
Why No Resize Support: Slab allocators pre-carve memory into fixed-size slots. There is no concept of “growing” or “shrinking” an object. All slots are identical. To change object size, you must free the old object and allocate from a different slab with the desired size.
Same Size Behavior: When old_bytes == new_bytes == obj_size, returns the same pointer with no reallocation. This is an O(1) no-op.
See also
alloc() Allocate fixed-size object
See also
return_element() Free object
See also
realloc_aligned() Realloc with alignment (also limited)
- Same Size (No-Op):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); memset(ptr, 'A', 256); // Realloc to same size auto new_ptr_result = slab->realloc(ptr, 256, 256, false); ASSERT_TRUE(new_ptr_result.hasValue()); EXPECT_EQ(new_ptr_result.value(), ptr); // Same pointer returned // Data preserved uint8_t* data = static_cast<uint8_t*>(new_ptr_result.value()); EXPECT_EQ(data[0], 'A'); slab->return_element(ptr);- Resize Rejected:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); // Try to grow auto grow = slab->realloc(ptr, 256, 512, false); EXPECT_FALSE(grow.hasValue()); // ERROR: cannot resize // Try to shrink auto shrink = slab->realloc(ptr, 256, 128, false); EXPECT_FALSE(shrink.hasValue()); // ERROR: cannot resize // Original pointer still valid slab->return_element(ptr);- Realloc from nullptr (Acts as alloc):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); void* ptr = nullptr; // Realloc from nullptr acts as alloc auto result = slab->realloc(ptr, 0, 256, true); ASSERT_TRUE(result.hasValue()); ptr = result.value(); // Memory is zero-initialized uint8_t* data = static_cast<uint8_t*>(ptr); EXPECT_EQ(data[0], 0); slab->return_element(ptr);- Realloc to Zero (Free):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); EXPECT_EQ(slab->in_use_blocks(), 1); // Realloc to zero frees the memory auto result = slab->realloc(ptr, 256, 0, false); ASSERT_TRUE(result.hasValue()); EXPECT_EQ(result.value(), nullptr); EXPECT_EQ(slab->in_use_blocks(), 0); // Freed- Manual Resize Pattern (Different Slabs):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Two slabs for different sizes auto slab128 = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); auto slab256 = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate 128-byte object auto old_ptr = slab128->alloc(128, false).value(); memcpy(old_ptr, "data", 4); // "Resize" to 256 bytes - manual process: // 1. Allocate from larger slab auto new_ptr = slab256->alloc(256, false).value(); // 2. Copy data (up to old size) memcpy(new_ptr, old_ptr, 128); // 3. Free old object slab128->return_element(old_ptr); // Now using 256-byte object slab256->return_element(new_ptr);Note
Slab allocators CANNOT resize objects. Both old_bytes and new_bytes must equal obj_size for the operation to succeed.
Note
To “resize” an object, you must: (1) Allocate from a slab with the new size, (2) Copy data, (3) Free the old object.
Note
The zeroed parameter has no effect when old_bytes == new_bytes since no new space is allocated.
- Parameters:
ptr – Pointer to existing allocation (or nullptr)
old_bytes – Current size of the allocation in bytes
new_bytes – Desired new size in bytes
zeroed – If true, zero-initialize any newly allocated space
- Returns:
Expected containing pointer on success, or error on failure
- Throws:
ArgumentError – If ptr is non-null but old_bytes is zero
ArgumentError – If old_bytes != obj_size or new_bytes != obj_size
- virtual Expected<void*> realloc_aligned(void *ptr, size_t old_bytes, size_t new_bytes, size_t alignment, bool zeroed = false) override
Realloc with alignment for slab allocator (limited - same size only)
Similar to realloc(), but with alignment verification. Slab allocators only support fixed-size objects, so this can only “realloc” to the same size while verifying alignment requirements.
Special Cases:
realloc_aligned(nullptr, 0, n, a) → Equivalent to alloc_aligned(n, a)
realloc_aligned(ptr, old, 0, a) → Frees ptr, returns nullptr (success)
realloc_aligned(ptr, old, new, a) where old == new == obj_size → Same ptr
realloc_aligned(ptr, old, new, a) where old != new → ERROR (cannot resize)
realloc_aligned(ptr, old, old, a) where a > slab_align → ERROR
Alignment Verification: The requested alignment must not exceed the slab’s alignment. If the pointer already satisfies the alignment (which it should, since all slots are pre-aligned), the same pointer is returned.
Same Size Behavior: When old_bytes == new_bytes == obj_size and alignment is satisfied, returns the same pointer with no reallocation (O(1) no-op).
See also
alloc_aligned() Allocate with alignment
See also
realloc() Realloc without alignment check
See also
return_element() Free object
- Same Size with Alignment Check:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Slab with 64-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto ptr = slab->alloc(256, false).value(); // Realloc same size with alignment verification auto new_ptr = slab->realloc_aligned(ptr, 256, 256, 64, false); ASSERT_TRUE(new_ptr.hasValue()); EXPECT_EQ(new_ptr.value(), ptr); // Same pointer // Alignment guaranteed uintptr_t addr = reinterpret_cast<uintptr_t>(new_ptr.value()); EXPECT_EQ(addr % 64, 0); slab->return_element(ptr);- Alignment Within Slab Alignment:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Slab with 128-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 128, 0).value(); auto ptr = slab->alloc(256, false).value(); // Request 64-byte alignment (within slab's 128) auto result = slab->realloc_aligned(ptr, 256, 256, 64, false); ASSERT_TRUE(result.hasValue()); EXPECT_EQ(result.value(), ptr); // Same pointer, satisfies alignment- Alignment Exceeds Slab (Error):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Slab with only 64-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto ptr = slab->alloc(256, false).value(); // Request 128-byte alignment (exceeds slab's 64) auto result = slab->realloc_aligned(ptr, 256, 256, 128, false); EXPECT_FALSE(result.hasValue()); // ERROR: alignment too large // Original pointer still valid slab->return_element(ptr);- Resize with Alignment Rejected:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto ptr = slab->alloc(256, false).value(); // Try to grow with alignment auto grow = slab->realloc_aligned(ptr, 256, 512, 64, false); EXPECT_FALSE(grow.hasValue()); // ERROR: cannot resize // Try to shrink with alignment auto shrink = slab->realloc_aligned(ptr, 256, 128, 64, false); EXPECT_FALSE(shrink.hasValue()); // ERROR: cannot resize slab->return_element(ptr);- Realloc from nullptr with Alignment:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); void* ptr = nullptr; // Realloc from nullptr acts as alloc_aligned auto result = slab->realloc_aligned(ptr, 0, 256, 64, true); ASSERT_TRUE(result.hasValue()); ptr = result.value(); // Aligned and zeroed uintptr_t addr = reinterpret_cast<uintptr_t>(ptr); EXPECT_EQ(addr % 64, 0); uint8_t* data = static_cast<uint8_t*>(ptr); EXPECT_EQ(data[0], 0); slab->return_element(ptr);- Manual Resize with Alignment (Different Slabs):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Two slabs with different sizes, both 64-byte aligned auto slab128 = SlabAllocator::WithBuddy(*buddy, 128, 64, 0).value(); auto slab256 = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); // Allocate 128-byte aligned object auto old_ptr = slab128->alloc_aligned(128, 64, false).value(); memset(old_ptr, 'A', 128); // "Resize" to 256 bytes with alignment preserved: // 1. Allocate from larger slab auto new_ptr = slab256->alloc_aligned(256, 64, false).value(); // 2. Copy data memcpy(new_ptr, old_ptr, 128); // 3. Free old slab128->return_element(old_ptr); // Verify alignment maintained uintptr_t addr = reinterpret_cast<uintptr_t>(new_ptr); EXPECT_EQ(addr % 64, 0); slab256->return_element(new_ptr);- Alignment Change Not Supported:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Slab with 64-byte alignment auto slab64 = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto ptr = slab64->alloc(256, false).value(); // Cannot "change" alignment to 128 (exceeds slab alignment) auto result = slab64->realloc_aligned(ptr, 256, 256, 128, false); EXPECT_FALSE(result.hasValue()); // ERROR // Solution: Allocate from slab with 128-byte alignment auto slab128 = SlabAllocator::WithBuddy(*buddy, 256, 128, 0).value(); auto new_ptr = slab128->alloc(256, false).value(); // Copy data memcpy(new_ptr, ptr, 256); // Free old slab64->return_element(ptr); // Now have 128-byte aligned object uintptr_t addr = reinterpret_cast<uintptr_t>(new_ptr); EXPECT_EQ(addr % 128, 0); slab128->return_element(new_ptr);Note
Slab allocators CANNOT resize objects. Both old_bytes and new_bytes must equal obj_size.
Note
Requested alignment must not exceed the slab’s alignment specified at creation.
Note
All slots are pre-aligned, so this method just validates the requirement is met - it doesn’t perform additional alignment.
Note
Changing alignment requirements cannot be satisfied if new alignment exceeds slab alignment. Create object in slab with appropriate alignment instead.
- Parameters:
ptr – Pointer to existing allocation (or nullptr)
old_bytes – Current size of the allocation in bytes
new_bytes – Desired new size in bytes
alignment – Required alignment in bytes (0 = max_align_t, rounded to power of 2)
zeroed – If true, zero-initialize any newly allocated space
- Returns:
Expected containing pointer on success, or error on failure
- Throws:
ArgumentError – If ptr is non-null but old_bytes is zero
ArgumentError – If old_bytes != obj_size or new_bytes != obj_size
AlignmentError – If alignment is invalid or exceeds slab’s alignment
- virtual void return_element(void *ptr, size_t bytes = 0, size_t alignment = 0) override
Free an object back to the slab allocator.
Frees a previously allocated object by returning it to the free list. The freed slot becomes immediately available for subsequent allocations.
Deallocation Process:
Validate pointer is not null (null is silently ignored)
Find which page contains the pointer (walk pages list)
Verify pointer is within slots region (not in page header)
Verify pointer is aligned on slot boundary
Cast pointer to Slot and push onto free list
Update accounting (decrease in_use_blocks, size)
Performance: O(p) where p is the number of pages (for page lookup), then O(1) to push onto free list. Typically O(1) amortized since most slabs have few pages.
No Coalescing: Unlike BuddyAllocator, slabs don’t coalesce. Freed slots simply go back on the free list and can be immediately reused. Pages remain allocated until the slab is destroyed.
See also
alloc() Allocate memory
See also
alloc_aligned() Allocate aligned memory
See also
reset() Bulk deallocation of all objects
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); // Use memory... memset(ptr, 0, 256); // Free when done slab->return_element(ptr); // ptr is now invalid - do not use!- Parameters Ignored:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto ptr = slab->alloc(256, false).value(); // All of these are equivalent (bytes and alignment ignored) slab->return_element(ptr); slab->return_element(ptr, 256, 0); slab->return_element(ptr, 0, 0); slab->return_element(ptr, 999, 123); // Values don't matter- NULL Pointer Handling:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); void* ptr = nullptr; // Safe - does nothing slab->return_element(ptr); // Conditional freeing is safe auto maybe_ptr = slab->alloc(256, false); if (maybe_ptr.hasValue()) { ptr = maybe_ptr.value(); // ... use ptr ... } // Safe even if allocation failed (ptr is nullptr) slab->return_element(ptr);- Multiple Objects:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate multiple objects std::vector<void*> ptrs; for (int i = 0; i < 20; ++i) { auto ptr = slab->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } EXPECT_EQ(slab->in_use_blocks(), 20); // Free all for (void* ptr : ptrs) { slab->return_element(ptr); } EXPECT_EQ(slab->in_use_blocks(), 0); // All slots are now in free list and can be reused EXPECT_EQ(slab->free_blocks(), slab->total_blocks());- Free Order Doesn’t Matter:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr1 = slab->alloc(256, false).value(); auto ptr2 = slab->alloc(256, false).value(); auto ptr3 = slab->alloc(256, false).value(); // Free in any order - doesn't matter for slabs slab->return_element(ptr2); // Middle first slab->return_element(ptr1); // Then first slab->return_element(ptr3); // Then last // All freed, all back in free list- Immediate Reuse:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); auto ptr1 = slab->alloc(128, false).value(); memset(ptr1, 'A', 128); // Free it slab->return_element(ptr1); // Immediately allocate again - likely gets same slot auto ptr2 = slab->alloc(128, false).value(); // May be same physical slot (LIFO free list) // But ptr1 is invalid - don't use it!- Invalid Pointer (Silent Failure):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // External pointer (not from this slab) void* external = malloc(256); slab->return_element(external); // Silent no-op (not from slab) free(external); // Allocate valid pointer auto ptr = slab->alloc(256, false).value(); // Misaligned pointer (offset into slot) uint8_t* misaligned = static_cast<uint8_t*>(ptr) + 10; slab->return_element(misaligned); // Silent no-op (misaligned) // Original pointer still valid slab->return_element(ptr); // OKNote
The bytes and alignment parameters are IGNORED by SlabAllocator. They exist only for base class interface compatibility. The slab knows the object size from its configuration.
Note
Passing nullptr is safe and does nothing (similar to free(nullptr)).
Note
Invalid pointers are handled silently - the function returns without error if the pointer is not from this slab, is misaligned, or is in the header region.
Note
Double-free results in undefined behavior. The allocator cannot detect all double-free scenarios.
Note
After this call, ptr becomes invalid. Accessing it results in undefined behavior.
Warning
Do not free pointers from a different SlabAllocator instance. Each slab manages its own distinct set of pages.
- Parameters:
ptr – Pointer to free (returned from alloc, alloc_aligned, realloc, or realloc_aligned)
bytes – Ignored (present for interface compatibility)
alignment – Ignored (present for interface compatibility)
- virtual bool stats(char *buffer, size_t buffer_size) const override
Generate diagnostic statistics report.
Generates a comprehensive human-readable report of the slab allocator’s current state, including geometry, capacity, usage, and per-page details.
Report Contents:
Object size and slot stride
Alignment requirements
Page size and header overhead
Number of pages and blocks per page
Total capacity and current usage (bytes and blocks)
Free block count (both geometric and from free list)
Utilization percentage
Per-page listing
Cross-Validation: The report includes free block count calculated two ways:
Geometric: total_blocks - in_use_blocks
Free list: Count by walking free list These should always match. Discrepancy indicates corruption.
Buffer Size: Typical reports are 500-1000 bytes. Recommend 2KB-4KB buffer to accommodate slabs with many pages. If buffer is too small, the function returns false.
See also
total_blocks() Get total capacity programmatically
See also
in_use_blocks() Get current usage programmatically
See also
free_blocks() Get free block count programmatically
See also
size() Get used bytes programmatically
See also
remaining() Get available bytes programmatically
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 4096).value(); // Allocate some objects for (int i = 0; i < 5; ++i) { slab->alloc(256, false); } // Generate report char buffer[2048]; bool ok = slab->stats(buffer, sizeof(buffer)); if (ok) { std::cout << buffer; }- Example Output:
Slab Statistics: Object size: 256 bytes Slot stride: 256 bytes Alignment: 64 bytes Page size: 4096 bytes Page header bytes: 64 Pages: 1 Blocks per page: 15 Total blocks: 15 In-use blocks: 5 Free blocks (geom): 10 Free blocks (free list): 10 Used: 1280 bytes Capacity: 3840 bytes Remaining: 2560 bytes Total (with overhead): 4096 bytes Utilization: 33.3% Page 1: 4096 bytes, 15 blocks- Monitoring Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate many objects std::vector<void*> ptrs; for (int i = 0; i < 50; ++i) { auto ptr = slab->alloc(128, false); if (ptr.hasValue()) { ptrs.push_back(ptr.value()); } } // Check statistics char buffer[4096]; slab->stats(buffer, sizeof(buffer)); std::cout << "After 50 allocations:\n" << buffer << "\n"; // Free half for (size_t i = 0; i < ptrs.size() / 2; ++i) { slab->return_element(ptrs[i]); } slab->stats(buffer, sizeof(buffer)); std::cout << "After freeing 25:\n" << buffer << "\n";- Detecting Corruption:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate and free normally auto ptr1 = slab->alloc(256, false).value(); auto ptr2 = slab->alloc(256, false).value(); slab->return_element(ptr1); char buffer[2048]; slab->stats(buffer, sizeof(buffer)); // Check for consistency std::string report(buffer); // Look for mismatched free counts // "Free blocks (geom): X" should match "Free blocks (free list): X" // If different, indicates corruption (double-free, list corruption, etc.)- Buffer Too Small:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); char small_buffer[100]; // Too small bool ok = slab->stats(small_buffer, sizeof(small_buffer)); if (!ok) { std::cerr << "Buffer too small for statistics report\n"; // Try larger buffer char large_buffer[4096]; ok = slab->stats(large_buffer, sizeof(large_buffer)); }- Empty Slab (No Allocations):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // No allocations yet - no pages allocated char buffer[2048]; slab->stats(buffer, sizeof(buffer)); // Output shows: // Pages: 0 // Total blocks: 0 // Capacity: 0 bytes // Utilization: N/A (capacity is 0)- Multiple Pages:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Small page size to force multiple pages auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 1024).value(); // Allocate enough to need multiple pages for (int i = 0; i < 20; ++i) { slab->alloc(256, false); } char buffer[4096]; slab->stats(buffer, sizeof(buffer)); // Output shows multiple pages: // Pages: 3 // Page 1: 1024 bytes, 3 blocks // Page 2: 1024 bytes, 3 blocks // Page 3: 1024 bytes, 3 blocks- Utilization Tracking:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); auto track_utilization = [&]() { char buffer[2048]; slab->stats(buffer, sizeof(buffer)); std::string report(buffer); size_t pos = report.find("Utilization: "); if (pos != std::string::npos) { std::string util_line = report.substr(pos); std::cout << util_line.substr(0, util_line.find('\n')) << "\n"; } }; // Track utilization over time for (int i = 0; i < 10; ++i) { slab->alloc(128, false); track_utilization(); }Note
This is a diagnostic tool for debugging and monitoring. It’s not intended for production hot paths due to formatting overhead.
Note
The report is human-readable, not machine-parseable. Don’t rely on exact format - use the query methods (total_blocks(), size(), etc.) for programmatic access.
- Parameters:
buffer – Character buffer to write report into
buffer_size – Size of the buffer in bytes
- Returns:
true if report generated successfully, false if buffer too small or nullptr
- virtual bool reset(bool trim = false) override
Bulk free all allocations and rebuild free list.
Performs a bulk deallocation of all objects by clearing accounting and rebuilding the free list from scratch. All outstanding allocations become invalid. Pages remain allocated (not returned to buddy).
Reset Process:
Validate slab geometry (sanity check)
Clear accounting (len_bytes = 0, size = 0)
Clear free list (free_list = nullptr)
Walk all pages and re-carve slots
Rebuild free list with all slots
Performance: O(pages × objects_per_page) - must walk every slot to rebuild list. Much faster than individual frees when deallocating many objects.
Pages Not Freed: Unlike trim operations in other allocators, reset() keeps all pages. The slab maintains its current capacity. Pages are only freed when the slab is destroyed.
Use Cases:
Per-frame allocations (game engines)
Request/response cycles (servers)
Test cleanup between test cases
Temporary scratch space that’s bulk-cleared
See also
return_element() Free individual object
See also
~SlabAllocator() Destructor that frees all pages
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate many objects for (int i = 0; i < 100; ++i) { slab->alloc(256, false); } EXPECT_EQ(slab->in_use_blocks(), 100); // Bulk free everything bool ok = slab->reset(); EXPECT_TRUE(ok); // All freed EXPECT_EQ(slab->in_use_blocks(), 0); EXPECT_EQ(slab->size(), 0); // Free list rebuilt - all slots available EXPECT_EQ(slab->free_blocks(), slab->total_blocks());- Per-Frame Pattern (Game Engine):
auto buddy = BuddyAllocator::Heap(10 * 1024 * 1024, 64, 0).value(); auto particles = SlabAllocator::WithBuddy(*buddy, sizeof(Particle), 0, 0).value(); while (game_running) { // Spawn particles this frame for (int i = 0; i < 50; ++i) { auto mem = particles->alloc(sizeof(Particle), false).value(); Particle* p = new (mem) Particle(); p->spawn(position); active_particles.push_back(p); } // Update and render... // Clean up all particles at end of frame for (Particle* p : active_particles) { p->~Particle(); // Call destructor } active_particles.clear(); // Bulk free - much faster than individual frees particles->reset(); }- Request/Response Pattern (Server):
auto buddy = BuddyAllocator::Heap(10 * 1024 * 1024, 64, 0).value(); auto request_pool = SlabAllocator::WithBuddy(*buddy, sizeof(RequestObject), 0, 0).value(); void handle_request(HttpRequest& req) { // Allocate temporary objects for this request auto obj1 = request_pool->alloc(sizeof(RequestObject), false).value(); auto obj2 = request_pool->alloc(sizeof(RequestObject), false).value(); // Process request... // At end of request, bulk free request_pool->reset(); }- Test Cleanup Pattern:
class MyTestFixture : public ::testing::Test { protected: UniquePtr<BuddyAllocator, BuddyDeleter> buddy; UniquePtr<SlabAllocator, SlabDeleter> slab; void SetUp() override { buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); } void TearDown() override { // Clean up after each test slab->reset(); } }; TEST_F(MyTestFixture, Test1) { auto ptr = slab->alloc(128, false); // Test... // reset() called automatically in TearDown() }- Pages Retained (No Trim):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate many - forces multiple page growth for (int i = 0; i < 100; ++i) { slab->alloc(256, false); } size_t capacity_before = slab->total_blocks(); // Reset slab->reset(); // Capacity unchanged - pages retained EXPECT_EQ(slab->total_blocks(), capacity_before); // But usage is zero EXPECT_EQ(slab->in_use_blocks(), 0);- Reset vs Individual Frees (Performance):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); std::vector<void*> ptrs; for (int i = 0; i < 1000; ++i) { ptrs.push_back(slab->alloc(128, false).value()); } // Option 1: Individual frees - O(n × p) where p = pages // for (void* ptr : ptrs) { // slab->return_element(ptr); // Each does page lookup // } // Option 2: Reset - O(p × slots_per_page) slab->reset(); // Much faster for bulk deallocation!- Multiple Reset Cycles:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Can reset multiple times for (int cycle = 0; cycle < 10; ++cycle) { // Allocate for (int i = 0; i < 50; ++i) { slab->alloc(256, false); } EXPECT_EQ(slab->in_use_blocks(), 50); // Reset slab->reset(); EXPECT_EQ(slab->in_use_blocks(), 0); }- Reset Before/After Statistics:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate for (int i = 0; i < 20; ++i) { slab->alloc(128, false); } char buffer[2048]; slab->stats(buffer, sizeof(buffer)); std::cout << "Before reset:\n" << buffer << "\n"; // Reset slab->reset(); slab->stats(buffer, sizeof(buffer)); std::cout << "After reset:\n" << buffer << "\n"; // Shows: // Before: In-use blocks: 20, Free blocks: X // After: In-use blocks: 0, Free blocks: total_blocks- Trim Parameter Ignored:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); for (int i = 0; i < 100; ++i) { slab->alloc(256, false); } size_t capacity = slab->total_blocks(); // Both are identical - trim has no effect slab->reset(false); EXPECT_EQ(slab->total_blocks(), capacity); slab->reset(true); // trim=true ignored EXPECT_EQ(slab->total_blocks(), capacity); // Still same capacityNote
The trim parameter is IGNORED. Slab allocators never trim pages back to the buddy allocator. Pages persist until slab destruction.
Note
After reset(), ALL pointers obtained from this slab become invalid. Accessing them results in undefined behavior.
Note
reset() is significantly faster than calling return_element() on each allocation individually when you need to free everything.
Note
The slab retains its full capacity. Subsequent allocations will reuse the existing pages without needing to grow.
Warning
All outstanding allocations become invalid. Do not use any pointers obtained before reset() after calling this function.
- Parameters:
trim – Ignored (present for interface compatibility - slabs don’t trim)
- Returns:
true if reset successful, false on error
- inline virtual void *save() const override
Save allocator state (not supported)
SlabAllocator does not support checkpoint/restore functionality. This method exists only for base class interface compatibility and always returns nullptr.
Why Not Supported: Slab allocators manage intrusive free lists with pointers embedded in freed slots. These pointers cannot be meaningfully saved and restored without complex serialization. Additionally, slabs are typically used for short-lived allocations where checkpointing doesn’t provide value.
Alternatives:
Manually track allocations if you need to revert state
Use BuddyAllocator if you need checkpoint support
See also
restore() Always returns false
See also
reset() Bulk deallocation alternative
- Usage (Always Fails):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate something auto ptr = slab->alloc(256, false); // Try to save state void* checkpoint = slab->save(); // Always nullptr EXPECT_EQ(checkpoint, nullptr);- Not Supported Pattern:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate some objects std::vector<void*> ptrs; for (int i = 0; i < 10; ++i) { ptrs.push_back(slab->alloc(128, false).value()); } // This pattern doesn't work with slab allocators void* checkpoint = slab->save(); // Returns nullptr // Allocate more for (int i = 0; i < 10; ++i) { ptrs.push_back(slab->alloc(128, false).value()); } // Cannot restore bool ok = slab->restore(checkpoint); // Always fails EXPECT_FALSE(ok);- Alternative - Manual Tracking:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // If you need to revert state, track allocations manually std::vector<void*> checkpoint_ptrs; // Phase 1: Initial allocations for (int i = 0; i < 5; ++i) { auto ptr = slab->alloc(256, false).value(); checkpoint_ptrs.push_back(ptr); } // Phase 2: Tentative allocations std::vector<void*> tentative; for (int i = 0; i < 10; ++i) { tentative.push_back(slab->alloc(256, false).value()); } // "Restore" by freeing tentative allocations for (void* ptr : tentative) { slab->return_element(ptr); } // Back to checkpoint state- Alternative - Use reset():
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Instead of checkpoint/restore, use reset() // Work cycle 1 for (int i = 0; i < 20; ++i) { slab->alloc(256, false); } slab->reset(); // Clear everything // Work cycle 2 for (int i = 0; i < 30; ++i) { slab->alloc(256, false); } slab->reset(); // Clear everything again- Why Checkpointing Doesn’t Work:
// Conceptual example of why save/restore is hard for slabs: // State 1: Allocate 3 objects auto ptr1 = slab->alloc(256, false).value(); // Slot A auto ptr2 = slab->alloc(256, false).value(); // Slot B auto ptr3 = slab->alloc(256, false).value(); // Slot C // Free list: Slot D -> Slot E -> ... // Now we want to "checkpoint" - what to save? // - Which slots are allocated? (ptr1, ptr2, ptr3) // - Free list structure? (intrusive pointers in freed slots) // - But slots can be reused, moved around... // Free ptr2 slab->return_element(ptr2); // Free list: Slot B -> Slot D -> Slot E -> ... // To restore, we'd need to: // - Know Slot B was freed after checkpoint // - Reconstruct exact free list state // - Mark ptr1, ptr3 as allocated // This is complex and not worth it for slab use casesNote
This method always returns nullptr. It exists only to satisfy the base Allocator interface.
Note
Calling this method has no side effects. The slab state is unchanged.
Note
Do not attempt to pass the return value to restore(). It will always be nullptr and restore() will always fail.
- Returns:
Always returns nullptr
- inline virtual bool restore(void *checkpoint) override
Restore allocator state (not supported)
SlabAllocator does not support checkpoint/restore functionality. This method exists only for base class interface compatibility and always returns false.
Why Not Supported: Since save() always returns nullptr, there is no valid checkpoint to restore. The intrusive free list structure makes meaningful checkpointing impractical for slab allocators.
Behavior: This method does nothing and always returns false, regardless of the checkpoint parameter value. The slab state is unchanged.
See also
save() Always returns nullptr
See also
reset() Bulk deallocation
- Usage (Always Fails):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate something auto ptr = slab->alloc(256, false); // Try to save and restore void* checkpoint = slab->save(); // Returns nullptr // More allocations... auto ptr2 = slab->alloc(256, false); // Try to restore bool ok = slab->restore(checkpoint); // Always fails EXPECT_FALSE(ok); // State is unchanged - both allocations still valid EXPECT_EQ(slab->in_use_blocks(), 2);- All Parameter Values Fail:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // All of these fail EXPECT_FALSE(slab->restore(nullptr)); void* fake_checkpoint = reinterpret_cast<void*>(0x12345678); EXPECT_FALSE(slab->restore(fake_checkpoint)); void* some_ptr = slab->alloc(128, false).value(); EXPECT_FALSE(slab->restore(some_ptr)); // None of these calls affect the slab state- Not a Replacement for Free:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); // This does NOT free the allocation slab->restore(nullptr); // ptr is still allocated EXPECT_EQ(slab->in_use_blocks(), 1); // Must use return_element() to free slab->return_element(ptr); EXPECT_EQ(slab->in_use_blocks(), 0);- Use reset() Instead:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // If you want to "restore" to empty state, use reset() // Allocate many objects for (int i = 0; i < 50; ++i) { slab->alloc(256, false); } EXPECT_EQ(slab->in_use_blocks(), 50); // "Restore" to empty state slab->reset(); EXPECT_EQ(slab->in_use_blocks(), 0);Note
This method always returns false. It exists only to satisfy the base Allocator interface.
Note
The checkpoint parameter is completely ignored. Any value (including nullptr) will result in the same behavior: no-op and return false.
Note
Calling this method has no side effects. The slab state is unchanged.
- Parameters:
checkpoint – Ignored (present for interface compatibility)
- Returns:
Always returns false
- virtual bool is_ptr(void *ptr) const override
Check if pointer is a valid allocation from this slab.
Validates whether a pointer points to a valid slot in this slab allocator by checking if it belongs to one of the slab’s pages and is properly aligned to a slot boundary.
Validation Checks:
Pointer is not null
Pointer belongs to one of this slab’s pages
Pointer is not in the page header region
Pointer is aligned on slot boundary (offset % slot_size == 0)
Limitations:
Cannot detect use-after-free (freed pointer still validates)
Cannot detect double-free
Cannot detect never-allocated slots
Only checks structural validity, not allocation state
See also
is_ptr_sized() Validate with size check
See also
return_element() Free pointer (validates internally)
- Valid Pointer:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); EXPECT_TRUE(slab->is_ptr(ptr)); slab->return_element(ptr); // Still valid (structural check only - can't detect freed state) EXPECT_TRUE(slab->is_ptr(ptr));- Invalid Pointers:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Null EXPECT_FALSE(slab->is_ptr(nullptr)); // External pointer void* external = malloc(256); EXPECT_FALSE(slab->is_ptr(external)); free(external); // Misaligned (offset into slot) auto ptr = slab->alloc(256, false).value(); uint8_t* misaligned = static_cast<uint8_t*>(ptr) + 10; EXPECT_FALSE(slab->is_ptr(misaligned)); slab->return_element(ptr);- Cross-Slab Check:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab1 = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto slab2 = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr1 = slab1->alloc(256, false).value(); // Valid in slab1 EXPECT_TRUE(slab1->is_ptr(ptr1)); // Invalid in slab2 (different slab) EXPECT_FALSE(slab2->is_ptr(ptr1)); slab1->return_element(ptr1);Note
This is a structural check only. A freed pointer will still return true if it’s a valid slot location.
Note
Performance is O(pages) due to page lookup.
- Parameters:
ptr – Pointer to validate
- Returns:
true if ptr is a valid slot from this slab, false otherwise
- virtual bool is_ptr_sized(void *ptr, size_t bytes) const override
Check if pointer can hold an allocation of given size.
Validates both pointer validity (via is_ptr()) and size compatibility. Since all slots in a slab are the same size, this checks if bytes <= obj_size.
Validation:
Validate ptr with is_ptr()
Check bytes <= obj_size
See also
is_ptr() Validate pointer only
- Valid Size:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); auto ptr = slab->alloc(256, false).value(); // Exact size EXPECT_TRUE(slab->is_ptr_sized(ptr, 256)); // Smaller sizes fit EXPECT_TRUE(slab->is_ptr_sized(ptr, 128)); EXPECT_TRUE(slab->is_ptr_sized(ptr, 1)); // Too large EXPECT_FALSE(slab->is_ptr_sized(ptr, 257)); EXPECT_FALSE(slab->is_ptr_sized(ptr, 512)); slab->return_element(ptr);- Invalid Pointer:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); void* external = malloc(256); // Pointer not from slab - always false regardless of size EXPECT_FALSE(slab->is_ptr_sized(external, 256)); EXPECT_FALSE(slab->is_ptr_sized(external, 128)); free(external);- Validation Before Use:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); void* ptr = get_pointer_from_somewhere(); size_t needed = 200; if (slab->is_ptr_sized(ptr, needed)) { // Safe to use ptr for 200 bytes memset(ptr, 0, needed); } else { // Invalid or too small }Note
All slots have the same size (obj_size). Any size <= obj_size fits.
Note
This is still a structural check - cannot detect if ptr is currently allocated or freed.
- Parameters:
ptr – Pointer to validate
bytes – Required size in bytes
- Returns:
true if ptr is valid and can hold bytes, false otherwise
- virtual size_t remaining() const noexcept override
Get remaining capacity in bytes.
Returns the total available capacity (allocated pages) minus bytes currently in use. This represents how much can be allocated before needing to grow a new page.
Calculated as: (total_blocks × obj_size) - size()
See also
size() Get bytes in use
See also
total_blocks() Get total slot capacity
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Initially: no pages allocated EXPECT_EQ(slab->remaining(), 0); // First allocation triggers page growth auto ptr1 = slab->alloc(256, false).value(); // Now has capacity (e.g., 15 slots × 256 = 3840 bytes) // Minus 1 in use (256 bytes) size_t avail = slab->remaining(); EXPECT_GT(avail, 0); // Allocate more auto ptr2 = slab->alloc(256, false).value(); // Remaining decreases EXPECT_EQ(slab->remaining(), avail - 256); slab->return_element(ptr1); slab->return_element(ptr2);- Capacity Tracking:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); for (int i = 0; i < 100; ++i) { size_t before = slab->remaining(); auto ptr = slab->alloc(128, false); if (ptr.hasValue()) { size_t after = slab->remaining(); if (before > 0) { // Used existing capacity EXPECT_EQ(after, before - 128); } else { // Grew new page EXPECT_GT(after, 0); } } }Note
This is capacity in allocated pages, not total memory available from the buddy allocator. Slab may grow beyond this.
- Returns:
Number of bytes available for allocation
- inline size_t stride() const noexcept
Get slot stride (internal slot size)
Returns the actual internal slot size used by the allocator. This is >= obj_size and >= sizeof(void*) to accommodate both the object and free list linkage.
Slot size is calculated as: max(align_up(obj_size, align), align_up(sizeof(Slot), align))
This is the actual memory consumed per allocation, including alignment padding and free list overhead.
See also
total_blocks() Get number of slots
- Object vs Slot Size:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // 100-byte objects, 64-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 100, 64, 0).value(); // obj_size = 100 (user-visible) // stride = 128 (actual slot size: next multiple of 64 >= 100) EXPECT_EQ(slab->stride(), 128); // stride is multiple of alignment EXPECT_EQ(slab->stride() % 64, 0);- Tiny Objects (Free List Overhead):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // 8-byte objects, 8-byte alignment auto slab = SlabAllocator::WithBuddy(*buddy, 8, 8, 0).value(); // obj_size = 8 // But stride >= sizeof(void*) (typically 8 on 64-bit) // So stride = 8 (satisfies both requirements) size_t stride = slab->stride(); EXPECT_GE(stride, 8); EXPECT_GE(stride, sizeof(void*));- Memory Overhead Calculation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 100, 0, 0).value(); // Trigger page allocation slab->alloc(100, false); size_t obj_size = 100; // User size size_t stride = slab->stride(); // Actual slot size size_t overhead_per_obj = stride - obj_size; std::cout << "Overhead per object: " << overhead_per_obj << " bytes\n"; size_t total_slots = slab->total_blocks(); size_t total_overhead = overhead_per_obj * total_slots; std::cout << "Total alignment overhead: " << total_overhead << " bytes\n";Note
stride() >= obj_size (user-visible size)
Note
stride() is a multiple of the slab’s alignment
Note
stride() >= sizeof(void*) for free list linkage
- Returns:
Size of each slot in bytes
- size_t total_blocks() const noexcept
Get total slot capacity (all allocated pages)
Returns the total number of slots available across all allocated pages. This represents the maximum number of objects that can be allocated before needing to grow a new page.
Calculated as: page_count × objs_per_slab
This includes both in-use and free slots.
See also
in_use_blocks() Get allocated slots
See also
free_blocks() Get available slots
See also
stride() Get bytes per slot
See also
remaining() Get remaining bytes
- Initial State (No Pages):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // No pages allocated yet EXPECT_EQ(slab->total_blocks(), 0); // First allocation triggers page growth auto ptr = slab->alloc(256, false).value(); // Now has capacity (e.g., 15 slots per page) EXPECT_GT(slab->total_blocks(), 0); slab->return_element(ptr);- Capacity Growth:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Small page to demonstrate growth auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 1024).value(); EXPECT_EQ(slab->total_blocks(), 0); // First allocation slab->alloc(256, false); size_t first_capacity = slab->total_blocks(); EXPECT_GT(first_capacity, 0); // e.g., 3 slots // Allocate beyond first page capacity for (int i = 1; i < 10; ++i) { slab->alloc(256, false); } // Should have grown EXPECT_GT(slab->total_blocks(), first_capacity);- Utilization Calculation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate some objects for (int i = 0; i < 10; ++i) { slab->alloc(128, false); } size_t total = slab->total_blocks(); size_t in_use = slab->in_use_blocks(); size_t free = slab->free_blocks(); // Verify accounting EXPECT_EQ(total, in_use + free); // Calculate utilization if (total > 0) { double util = 100.0 * in_use / total; std::cout << "Utilization: " << util << "%\n"; }- Capacity Never Decreases:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate many std::vector<void*> ptrs; for (int i = 0; i < 100; ++i) { ptrs.push_back(slab->alloc(256, false).value()); } size_t capacity_peak = slab->total_blocks(); // Free all for (void* ptr : ptrs) { slab->return_element(ptr); } // Capacity unchanged (pages retained) EXPECT_EQ(slab->total_blocks(), capacity_peak); // But nothing in use EXPECT_EQ(slab->in_use_blocks(), 0);Note
Returns 0 if no pages allocated yet (lazy allocation)
Note
Increases when new pages are allocated (via grow())
Note
Never decreases (pages not freed until slab destruction)
- Returns:
Total number of slots across all pages
- size_t free_blocks() const noexcept
Get number of free (available) slots.
Returns the count of slots available for allocation by walking the free list and counting nodes.
Performance: O(free_blocks) - walks entire free list
Verification: Should equal (total_blocks - in_use_blocks). If not, indicates corruption.
See also
total_blocks() Get total capacity
See also
in_use_blocks() Get allocated slots
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Initially: no pages, no free blocks EXPECT_EQ(slab->free_blocks(), 0); // First allocation triggers page growth auto ptr = slab->alloc(256, false).value(); // Now has free slots (e.g., 15 total - 1 used = 14 free) EXPECT_GT(slab->free_blocks(), 0); // Free the allocation slab->return_element(ptr); // All slots free again EXPECT_EQ(slab->free_blocks(), slab->total_blocks());- Tracking Free Slots:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate several std::vector<void*> ptrs; for (int i = 0; i < 5; ++i) { ptrs.push_back(slab->alloc(128, false).value()); } size_t free_after_alloc = slab->free_blocks(); // Free 3 for (int i = 0; i < 3; ++i) { slab->return_element(ptrs[i]); } // 3 more free EXPECT_EQ(slab->free_blocks(), free_after_alloc + 3); // Cleanup for (int i = 3; i < 5; ++i) { slab->return_element(ptrs[i]); }- Verification (Corruption Detection):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate some for (int i = 0; i < 10; ++i) { slab->alloc(256, false); } size_t total = slab->total_blocks(); size_t in_use = slab->in_use_blocks(); size_t free = slab->free_blocks(); // Should always hold EXPECT_EQ(total, in_use + free); // If mismatch, indicates corruption if (total != in_use + free) { std::cerr << "CORRUPTION DETECTED!\n"; }- After Reset:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate many for (int i = 0; i < 50; ++i) { slab->alloc(256, false); } size_t total = slab->total_blocks(); // Some free, some used EXPECT_LT(slab->free_blocks(), total); // Reset slab->reset(); // All free EXPECT_EQ(slab->free_blocks(), total);Note
This is an O(n) operation where n = number of free slots. For programmatic use, consider caching the result.
- Returns:
Number of slots currently in the free list
- size_t in_use_blocks() const noexcept
Get number of allocated (in-use) slots.
Returns the count of slots currently in use, calculated from the tracked byte usage divided by object size.
Calculated as: size() / obj_size
Performance: O(1) - simple division of tracked value
See also
total_blocks() Get total capacity
See also
free_blocks() Get available slots
See also
size() Get bytes in use (in_use_blocks × obj_size)
- Basic Usage:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Initially: nothing allocated EXPECT_EQ(slab->in_use_blocks(), 0); // Allocate auto ptr1 = slab->alloc(256, false).value(); EXPECT_EQ(slab->in_use_blocks(), 1); auto ptr2 = slab->alloc(256, false).value(); EXPECT_EQ(slab->in_use_blocks(), 2); // Free one slab->return_element(ptr1); EXPECT_EQ(slab->in_use_blocks(), 1); // Free both slab->return_element(ptr2); EXPECT_EQ(slab->in_use_blocks(), 0);- Tracking Allocations:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); std::vector<void*> ptrs; // Allocate 20 for (int i = 0; i < 20; ++i) { ptrs.push_back(slab->alloc(128, false).value()); EXPECT_EQ(slab->in_use_blocks(), i + 1); } EXPECT_EQ(slab->in_use_blocks(), 20); // Free all for (void* ptr : ptrs) { slab->return_element(ptr); } EXPECT_EQ(slab->in_use_blocks(), 0);- Utilization Calculation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate some for (int i = 0; i < 10; ++i) { slab->alloc(256, false); } size_t total = slab->total_blocks(); size_t in_use = slab->in_use_blocks(); if (total > 0) { double utilization = 100.0 * in_use / total; std::cout << "Utilization: " << utilization << "%\n"; if (utilization > 90.0) { std::cout << "Warning: High utilization\n"; } }- Invariants:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 0).value(); // Allocate some for (int i = 0; i < 15; ++i) { slab->alloc(128, false); } size_t in_use = slab->in_use_blocks(); size_t bytes = slab->size(); // Invariant: in_use_blocks × obj_size == size() EXPECT_EQ(in_use * 128, bytes); // Invariant: in_use <= total EXPECT_LE(in_use, slab->total_blocks());- After Reset:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Allocate many for (int i = 0; i < 50; ++i) { slab->alloc(256, false); } EXPECT_EQ(slab->in_use_blocks(), 50); // Reset slab->reset(); // Nothing in use EXPECT_EQ(slab->in_use_blocks(), 0); EXPECT_EQ(slab->size(), 0);Note
This is O(1) unlike free_blocks() which is O(n).
- Returns:
Number of slots currently allocated to users
Public Static Functions
- static Expected<cslt::UniquePtr<SlabAllocator, SlabDeleter>> WithBuddy(BuddyAllocator &buddy, size_t obj_size, size_t align = 0, size_t slab_bytes_hint = 0)
Factory method to create a slab allocator backed by a buddy allocator.
Creates a slab allocator that carves fixed-size objects from pages allocated from the buddy allocator. The allocator grows lazily - pages are only allocated when the free list is exhausted.
Initialization Process:
Validate and normalize parameters (obj_size > 0, align to power of 2)
Allocate SlabAllocator structure from buddy
Calculate slot size (max of obj_size and free list pointer size, aligned)
Calculate page geometry (header size, objects per page)
Return empty slab (no pages allocated yet - lazy allocation)
Parameter Normalization:
align=0 → alignof(max_align_t) (typically 16 bytes)
align=non-pow2 → rounded up to next power of 2 (e.g., 48 → 64)
slab_bytes_hint=0 → max(4096, obj_size * 64 + page_header)
Slot size adjusted to hold both object and free list linkage
Page Size Calculation: When slab_bytes_hint=0, the default is the larger of:
4096 bytes (1 page)
Enough space for 64 objects plus page header
The final page size is adjusted so slots fit evenly with no tail waste.
Lazy Allocation: No pages are allocated during initialization. The first call to alloc() will trigger page allocation via grow_().
See also
~SlabAllocator() Destructor that frees pages back to buddy
See also
alloc() Allocate a fixed-size object from the slab
See also
return_element() Free an object back to the slab
See also
reset() Bulk free all allocations
- Basic Creation:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Create slab for 256-byte objects with defaults auto slab_result = SlabAllocator::WithBuddy(*buddy, 256, 0, 0); if (slab_result.hasValue()) { auto slab = cslt::move(slab_result.value()); // Use slab... } else { std::cerr << "Error: " << slab_result.error().what() << "\n"; }- Custom Alignment:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // 64-byte alignment for cache-line aligned structures auto slab = SlabAllocator::WithBuddy(*buddy, 256, 64, 0).value(); auto obj = slab->alloc(256, false).value(); assert(reinterpret_cast<uintptr_t>(obj) % 64 == 0);- Custom Page Size:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Use 8KB pages instead of default 4KB auto slab = SlabAllocator::WithBuddy(*buddy, 128, 0, 8192).value(); // First allocation triggers page growth auto obj = slab->alloc(128, false); // Check how many objects fit per page std::cout << "Objects per page: " << slab->total_blocks() / 1 << "\n";- Small Objects (Slot Size Adjustment):
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // 8-byte objects, but slots will be larger auto slab = SlabAllocator::WithBuddy(*buddy, 8, 0, 0).value(); // stride() returns actual slot size std::cout << "Object size: 8\n"; std::cout << "Slot size: " << slab->stride() << "\n"; // e.g., 16 (for pointer)- Alignment Normalization:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Request 48-byte alignment (not power of 2) auto slab = SlabAllocator::WithBuddy(*buddy, 256, 48, 0).value(); // Automatically rounded to 64 (next power of 2) auto obj = slab->alloc(256, false).value(); assert(reinterpret_cast<uintptr_t>(obj) % 64 == 0);- Multiple Slabs from Same Buddy:
auto buddy = BuddyAllocator::Heap(16 * 1024 * 1024, 64, 0).value(); // Create slabs for different object sizes auto particles = SlabAllocator::WithBuddy(*buddy, sizeof(Particle), 0, 0).value(); auto projectiles = SlabAllocator::WithBuddy(*buddy, sizeof(Projectile), 0, 0).value(); auto enemies = SlabAllocator::WithBuddy(*buddy, sizeof(Enemy), 0, 0).value(); // Each slab manages its own object size auto p = particles->alloc(sizeof(Particle), false); auto proj = projectiles->alloc(sizeof(Projectile), false); auto e = enemies->alloc(sizeof(Enemy), false); // All slabs share the same buddy allocator for page management- Error Handling:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // Invalid: zero object size auto result1 = SlabAllocator::WithBuddy(*buddy, 0, 0, 0); if (!result1.hasValue()) { std::cerr << result1.error().what() << "\n"; // "Object size must be non-zero" } // Invalid: alignment too large auto result2 = SlabAllocator::WithBuddy(*buddy, 256, SIZE_MAX / 2 + 1, 0); if (!result2.hasValue()) { std::cerr << result2.error().what() << "\n"; // "Alignment exceeds maximum" }- Page Size vs Object Count:
auto buddy = BuddyAllocator::Heap(1024 * 1024, 64, 0).value(); // With 256-byte objects and 4KB default pages: // - Page header: ~64 bytes (aligned) // - Usable: 4096 - 64 = 4032 bytes // - Objects per page: 4032 / 256 = 15 objects // - Actual page size: 64 + 15*256 = 3904 bytes (no tail waste) auto slab = SlabAllocator::WithBuddy(*buddy, 256, 0, 0).value(); // Trigger first page allocation slab->alloc(256, false); std::cout << "Objects per page: " << slab->total_blocks() << "\n"; // 15Note
The buddy allocator must outlive the slab allocator. The slab stores a non-owning pointer to buddy and will return pages to it during destruction.
Note
obj_size cannot be changed after creation. To allocate different sizes, create separate slab allocators.
Note
The SlabAllocator structure itself is allocated from the buddy allocator, not from the system heap.
- Parameters:
buddy – Reference to backing BuddyAllocator (non-owning, must outlive slab)
obj_size – Size of each object in bytes (fixed for lifetime of slab)
align – Slot alignment in bytes (0 = alignof(max_align_t), rounded to power of 2)
slab_bytes_hint – Suggested page size in bytes (0 = automatic: 4KB or 64 objects)
- Returns:
Expected containing UniquePtr to SlabAllocator on success, or error on failure
- Throws:
ArgumentError – If obj_size is zero
AlignmentError – If alignment is invalid or too large (> SIZE_MAX/2)
MemoryError – If buddy allocator cannot allocate the SlabAllocator structure
SlabDeleter
An SlabAllocator class can have it memory manually freed or be passed
between scopes. However, if the user does not manually free the memory, it
will be automatically freed after it leaves its originating scope.
The SlabDeleter struct is a data structure used to destroy Arena memory
after it goes out of scope. While this is a publically available struct, the
struct is automatically invoked via a UniquePtr when an SlabAllocator
is initialized, and the user does not need to interact with it.
- struct SlabDeleter
Public Functions
- inline void operator()(SlabAllocator *slab) const noexcept
Destroy and deallocate a SlabAllocator instance.
Implements the custom deletion logic for SlabAllocator:
Saves buddy pointer before destruction
Calls destructor to free pages
Frees slab structure back to buddy allocator
This ensures the SlabAllocator memory is returned to the correct allocator (buddy, not operator delete).
Note
Called automatically by UniquePtr<SlabAllocator, SlabDeleter> when the pointer goes out of scope or is reset.
- Parameters:
slab – SlabAllocator to destroy (may be nullptr)