Immutable release entry — part of the project changelog. One file per release; format and rationale in ADR-0038. Released entries are never edited.
0.3.0 — 2026-06-13
Milestone 3 — C++ Wrapper & Type Safety. A C++-ergonomics milestone layered on
the v0.2.0 single-threaded core: the C ABI and its O(1) algorithm are unchanged. The
C++ surface gains a resolved exception policy at the C/C++ boundary (the dual-verb
allocate / try_allocate convention, with the Pool constructor now throwing
std::bad_alloc on failure), the type-safe TypedPool<T>, an STL-compatible
PoolAllocator<T> Adapter that lets standard and custom containers draw storage
from a pool, and a read-only free-list diagnostic Iterator gated out of release
builds. Four new Architecture Decision Records (0016–0019) take the running total to
19, and the patterns catalogue gains Adapter and Iterator as Implemented. No
Spec Coverage Map row flips: Milestone 3 is C++-side ergonomics over an already-✅
core — §2.2's "std::bad_alloc (C++)" half is now satisfied by ADR-0016, but the row
stays 🚧 pending the dynamic-growth half (Milestone 5). Full release notes in
docs/releases/v0.3.0.md.
- ADR-0016 — exception
policy at the C/C++ boundary: the C ABI is exception-free forever (every C failure
is
NULL/ no-op), and the C++ surface adopts a dual-verb convention where the spec §2.2 "configurable knob" is resolved per call site, not per build. Pool::try_allocate()— newnoexceptallocation verb returningnullptron exhaustion or on an empty (moved-from) wrapper; the exactPool::allocate()semantics of v0.2.0.
- ADR-0017 and
typed_pool.hpp—it::d4np::memorypool::TypedPool<T>, the header-only type-safe pool: the spec-conformantblock_sizeis derived fromTat compile time (ADR-0009 §2 satisfied by construction, over-alignedTrejected with astatic_assert), the typed storage verbs follow the ADR-0016 dual-verb policy, and theconstruct/destroyobject-lifetime pair offers the strong exception guarantee on throwingTconstructors. Dedicatedtyped_poolCTest binary with eightTEST_CASEs.
- ADR-0018 and
pool_allocator.hpp—it::d4np::memorypool::PoolAllocator<T>, the header-only STL-compatible allocator Adapter. It satisfies the Cpp17Allocator requirements and is a non-owning back-reference to aPool(singlePool*member,sizeof == sizeof(void*); the pool must out-live every container and adapter copy). Single-block requests (n == 1, fitting, not over-aligned) route to the pool in O(1) —std::bad_allocon exhaustion per ADR-0016 §2 — while everything else (n > 1, oversized / over-alignedT, rebound nodes larger than the block) falls back to over-aligned::operator new/::operator delete. The routing predicate is a pure function of(n, sizeof(T), alignof(T), block_size), sodeallocatereturns each pointer through the path that allocated it with no per-pointer bookkeeping.std::list/std::map/std::setrun on the pool fast path;std::vectorruns on the fallback. - Propagation traits specified per ADR-0018 §4:
propagate_on_container_copy_assignment,propagate_on_container_move_assignment, andpropagate_on_container_swapare allstd::false_type;is_always_equalisstd::false_type(stateful);operator==compares the underlyingPoolidentity. - Public C function
memory_pool_block_size(const memory_pool_t*)— reports the configured per-block size, O(1),NULL-tolerant (returns 0), ANSI C C89-compatible; the introspection companion tomemory_pool_metadata_bytesthat backs the adapter's size-fit decision (ADR-0018 §3). C++ forwarder[[nodiscard]] std::size_t Pool::block_size() const noexceptadded in lock-step; the accessor is exercised byc_consumer_min.cunder the C89/C99 CI jobs. - Dedicated
pool_allocatorCTest binary (pool_allocator_test.cpp) with sevenTEST_CASEs: pool-fast-path exhaustion, multi-block + oversized-Tfallback leaving the pool untouched, equality / statefulness / rebinding, the propagation-traitstatic_asserts, and end-to-endstd::list(pool path) +std::vector(fallback) round-trips. - Adapter added to
docs/patterns/README.mdAdopted / Planned table as row #5, statusImplemented.
- ADR-0019 and
free_list_iterator.hpp—it::d4np::memorypool::FreeListIterator/FreeListView, a read-only Iterator (LegacyForwardIterator) over the implicit free list for diagnostics.value_typeisconst void*(a free-slot address);FreeListViewis the range adaptor (begin()/end(), constructible from aconst memory_pool_t*or aPool&) so the walk composes with range-for,std::distance,std::find, and the rest of<algorithm>. Diagnostics-only: the walk is O(free_count) and must never touch the allocation hot path. - The entire diagnostic surface is gated behind the
PBR_MEMORY_POOL_DIAGNOSTICSmacro (ADR-0019 §1), defaulting to1in debug builds (!NDEBUG) and0in release builds (NDEBUG); an explicit definition wins. The new CMake optionPBR_MEMORY_POOL_ENABLE_DIAGNOSTICS(defaultOFF) is the documented opt-in — whenONit forces the macro to1as a PUBLIC compile definition onpbr_memory_poolso the library and every linking consumer agree. - Three gated public C accessors backing the traversal:
memory_pool_debug_free_list_head,memory_pool_debug_free_list_next, andmemory_pool_debug_free_count— NULL-tolerant,const-correct, ANSI C C89-compatible. They keep the ADR-0009 §1 next-link layout encapsulated insidememory_pool.cpp(Pimpl boundary intact), and are exercised byc_consumer_min.cunder the C89/C99 CI jobs (under the same macro guard). - Dedicated
free_list_iteratorCTest binary (free_list_iterator_test.cpp) with five cases when diagnostics are enabled (ascending strided walk of a fresh pool withfree_countvsstd::distancecross-check, allocation shrinks the list, a freed block returns to the head and is walked first, exhausted-pool empty range, LegacyForwardIterator behaviours) plus a placeholder case when the surface is gated out, so the binary builds in every configuration. - Iterator added to
docs/patterns/README.mdAdopted / Planned table as row #6, statusImplemented.
- Dedicated
container_integrationCTest binary (container_integration_test.cpp) exercising the M3.3PoolAllocator<T>(ADR-0018) end-to-end throughstd::list(pool fast path, with diagnostics-gated free-count delta assertions andstd::stringelements for non-trivial construct/destroy),std::vector(heap fallback, contents/growth/copy/<algorithm>interop), and a small hand-written allocator-awareForwardList<T, Allocator>driven with bothstd::allocatorandPoolAllocator. SevenTEST_CASEs. - The pool-sizing recipe for node-based containers (deferred to M3.5 by
ADR-0018 §3) is documented as a worked example in the test file header:
block_size ≥ sizeof(rebound node); an undersized pool degrades safely to the heap fallback. The comprehensive README usage section remains M7.2.
- Breaking (pre-1.0):
Pool::allocate()now throwsstd::bad_allocon exhaustion (and on a moved-from wrapper) instead of returningnullptr— migration:allocate()→try_allocate()for the in-band-failure behaviour. - Breaking (pre-1.0): the
Pool(block_size, block_count)constructor now throwsstd::bad_allocwhen the underlyingmemory_pool_createfails, retiring the ADR-0010 §2 silent-empty-state semantics — migration: usePool::makeorPoolBuilder::buildfor failure-as-a-value construction.Pool::makeis restructured around a private adopt-handle constructor so the non-throwing path contains no try/catch. - The microbenchmark's timed loops call
try_allocate()instead ofallocate()— the apples-to-apples comparison againstmalloc's in-bandNULL, byte-identical to the code path that produced the committed v0.2.0 numbers (ADR-0016 §4).
None. Milestone 3 adds C++-side ergonomics over the already-✅ single-threaded core,
so no traceability row changes state. ADR-0016 satisfies the "std::bad_alloc (C++)"
clause of §2.2, but that row stays 🚧 because its dynamic-growth clause lands in
Milestone 5. Coverage at the close of Milestone 3 is unchanged from v0.2.0: eight ✅,
one 🚧 (§6.3), and §2.2 / §2.4 / §6.3-concurrent in flight for Milestones 4–5.