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:

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";             // ~704

Note

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

  • bytesExpected 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.

{.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

  • bytesExpected 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 buddy and 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:

  1. Calls the arena’s destructor (frees additional chunks)

  2. 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:

Expected with FeatureDisabledError

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:

Expected with FeatureDisabledError

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:

  1. Calls the pool’s destructor (cleans up pool state)

  2. 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:
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:

  1. Pointer is within freelist memory region

  2. Valid FreeListHeader exists before the pointer

  3. Block size and offset are sane

  4. 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:

  1. Clears usage accounting (len_ = 0, size_ = 0)

  2. Resets high-water mark (cur_ = memory_)

  3. Destroys free list

  4. 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

See also

used() To verify reset cleared allocations (should be 0)

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:

  1. Pointer is not NULL

  2. Pointer is within the freelist’s memory region

  3. Pointer has room for FreeListHeader before it

  4. Valid header exists with sane block_size and offset

  5. Reconstructed block boundaries fit within memory region

  6. 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:

  1. Requested bytes is greater than 0

  2. Block’s user data region is large enough (user_data_size >= bytes)

  3. 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.

See also

is_ptr() For basic pointer validation without size check

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

restore() Corresponding restore method (also unsupported)

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

save() Corresponding save method (also unsupported)

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

ArenaDeleter

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:

  1. Extract ownership information (owns_arena, arena, mem_type)

  2. Call ~FreeListAllocator() destructor

  3. 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 ONLY flag.

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 coalescing

Note

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:

  1. Free OS-backed memory pool (via os_free)

  2. Free free-lists array (via delete[])

  3. Call this destructor

  4. 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:

  1. Calculate total size needed: bytes + sizeof(BuddyHeader)

  2. Round to next power of 2 (minimum: min_block_size)

  3. Find free block of appropriate size (or larger)

  4. Split larger blocks if necessary (recursive division by 2)

  5. Place header at block start, return pointer after header

  6. 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:
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:

  1. Normalize alignment to power of 2 (e.g., 48 → 64)

  2. Calculate total: bytes + header + (alignment - 1) for worst-case padding

  3. Round to next power of 2 (minimum: min_block_size)

  4. Find and allocate block (with splitting if needed)

  5. Find aligned position within block

  6. Place header immediately before aligned position

  7. 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:
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:
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:
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:

  1. Validate pointer is not null (null is silently ignored)

  2. Read BuddyHeader located 16 bytes before the user pointer

  3. Validate header (order within valid range, offset within pool)

  4. Calculate block start from header’s block_offset

  5. Update accounting (decrease size_)

  6. Begin coalescing process

Coalescing Algorithm: The allocator recursively merges blocks with their buddies:

  1. Calculate buddy offset: current_offset XOR (2^current_order)

  2. Check if buddy is in the free list at this level

  3. 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

  4. 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_order

Performance: 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:

  1. Validate allocator is initialized (base, pool_size, num_levels valid)

  2. Clear all free lists (set all levels to nullptr)

  3. Create initial free block at pool base

  4. Place block in top-level free list (max_order)

  5. 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:

  • ptr is non-null

  • ptr lies within the allocator’s managed memory pool

  • The internal BuddyHeader immediately preceding ptr is readable

  • The header encodes a valid block order for this allocator

  • The block offset and size are consistent with the pool boundaries

  • ptr lies within the bounds of the referenced allocation block

This 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 ptr is 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 ptr is 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 ptr has sufficient usable capacity for bytes.

Validation steps include:

  • All structural checks performed by is_ptr()

  • Determination of the allocation block size from the internal header

  • Verification that bytes does not exceed the block’s usable payload (block size minus header)

This function is memory-safe and will not dereference invalid memory, even if ptr is 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 bytes parameter 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 ptr is a valid user pointer returned by this BuddyAllocator and the underlying allocation can accommodate at least bytes, 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 buffer is null or buffer_size is 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 buffer in 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

size() Query currently allocated bytes

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.

See also

restore() Corresponding restore operation (also unsupported)

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

save() Corresponding save operation (unsupported)

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:

  1. Validates and normalizes parameters (rounds to powers of 2)

  2. Allocates OS memory pool

  3. Initializes free-list array (one per size level)

  4. Places initial large free block at top level

  5. 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:

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:

  1. Release the OS-backed memory pool used for allocations (via BuddyAllocator::os_free)

  2. Free the free-lists array allocated during initialization

  3. Invoke the BuddyAllocator destructor

  4. 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 ONLY flag.

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:

  1. Validate bytes == obj_size (strict size requirement)

  2. If free_list empty, call grow_() to allocate a new page

  3. Pop first slot from free_list (O(1) operation)

  4. Optionally zero-initialize the memory

  5. 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:

  1. Validate bytes == obj_size (strict size requirement)

  2. Normalize alignment (0 → max_align_t, round to power of 2)

  3. Verify requested_align <= slab’s align (pre-aligned slots)

  4. Delegate to alloc() to pop from free list

  5. 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:
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:
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:

  1. Validate pointer is not null (null is silently ignored)

  2. Find which page contains the pointer (walk pages list)

  3. Verify pointer is within slots region (not in page header)

  4. Verify pointer is aligned on slot boundary

  5. Cast pointer to Slot and push onto free list

  6. 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);  // OK

Note

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:

  1. Validate slab geometry (sanity check)

  2. Clear accounting (len_bytes = 0, size = 0)

  3. Clear free list (free_list = nullptr)

  4. Walk all pages and re-carve slots

  5. 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 capacity

Note

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:

  • Use reset() for bulk deallocation instead of restore()

  • 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 cases

Note

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:

  1. Pointer is not null

  2. Pointer belongs to one of this slab’s pages

  3. Pointer is not in the page header region

  4. 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:

  1. Validate ptr with is_ptr()

  2. 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:

  1. Validate and normalize parameters (obj_size > 0, align to power of 2)

  2. Allocate SlabAllocator structure from buddy

  3. Calculate slot size (max of obj_size and free list pointer size, aligned)

  4. Calculate page geometry (header size, objects per page)

  5. 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";  // 15

Note

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:

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:

  1. Saves buddy pointer before destruction

  2. Calls destructor to free pages

  3. 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:

slabSlabAllocator to destroy (may be nullptr)