Third tagged release of pbr-cpp-memory-pool. Milestone 3 is a C++-ergonomics layer over the v0.2.0 single-threaded core: the C ABI, its O(1) free-list algorithm, and the committed benchmark numbers are unchanged. What's new is an idiomatic, type-safe, allocator-aware C++ surface — and a diagnostic iterator for tests — built entirely on top of the existing four spec §5 functions.
A resolved exception policy at the C/C++ boundary (ADR-0016)
The spec §2.2 "return NULL or throw in C++" question is answered with a dual-verb convention, resolved per call site rather than per build:
- The C ABI stays exception-free forever — every C failure is
NULL/ no-op. Pool::allocate()now throwsstd::bad_allocon exhaustion (and on a moved-from wrapper).- The new
Pool::try_allocate()isnoexceptand returnsnullptr— the exact v0.2.0allocate()semantics, preserved under a new name. - The
Pool(block_size, block_count)constructor now throwsstd::bad_allocwhenmemory_pool_createfails, retiring the ADR-0010 silent-empty-state.Pool::make/PoolBuilder::buildremain the failure-as-a-value path, restructured around a private adopt-handle constructor so the non-throwing route contains no try/catch.
Breaking (pre-1.0): code relying on
allocate()returningnullptron exhaustion should migrate totry_allocate(); code relying on the silent-empty-state constructor should usePool::make/PoolBuilder.
TypedPool<T> — the type-safe pool (ADR-0017)
it::d4np::memorypool::TypedPool<T> is a header-only template composing Pool. The spec-conformant block_size is derived from T at compile time (max(sizeof(T), sizeof(void*)) rounded up to alignof(std::max_align_t)), so every ADR-0009 §2 precondition holds by construction; over-aligned T is rejected with a static_assert. Its two-layer surface separates storage (allocate / try_allocate / deallocate, the ADR-0016 verbs returning typed uninitialized slots) from object lifetime (construct(Args&&...) / destroy, which placement-new and destroy a T). construct provides the strong exception guarantee: if T's constructor throws, the slot returns to the free list before the exception propagates.
it::d4np::memorypool::TypedPool<MyType> pool(1024);
MyType* obj = pool.construct(arg1, arg2); // throws std::bad_alloc on exhaustion
// ... use obj ...
pool.destroy(obj); // runs ~MyType() and recycles the slotPoolAllocator<T> — STL-compatible allocator Adapter (ADR-0018)
it::d4np::memorypool::PoolAllocator<T> satisfies the Cpp17Allocator requirements so standard and custom containers can draw their storage from a Pool. It is the structural Adapter pattern: a non-owning back-reference to a Pool (sizeof == sizeof(void*); the pool must out-live every container and adapter copy). A request routes to the pool iff it is a single block that fits — n == 1 && sizeof(T) <= pool.block_size() && alignof(T) <= alignof(std::max_align_t) — served by the pool in O(1) (std::bad_alloc on exhaustion); everything else (n > 1, oversized / over-aligned T, rebound nodes larger than the block) falls back to over-aligned ::operator new / ::operator delete.
Because the standard guarantees deallocate(p, n) is called with the same n and type as the matching allocate, and block_size() is invariant for the pool's lifetime, the routing predicate evaluates identically at both ends — every pointer is freed by exactly the path that allocated it, with no per-pointer bookkeeping. So std::list / std::map / std::set run on the pool fast path, while std::vector runs on the fallback; both are memory-correct. The propagation traits are all std::false_type, is_always_equal is std::false_type, and operator== compares the underlying Pool identity.
A new introspection accessor size_t memory_pool_block_size(const memory_pool_t*) (O(1), NULL-tolerant, ANSI C C89-compatible — the companion to memory_pool_metadata_bytes) backs the size-fit decision, with a Pool::block_size() forwarder.
it::d4np::memorypool::Pool pool(64, 1024);
std::list<int, it::d4np::memorypool::PoolAllocator<int>> values{
it::d4np::memorypool::PoolAllocator<int>(pool)};
values.push_back(42); // node allocated from the pool in O(1)A read-only free-list diagnostic Iterator (ADR-0019)
FreeListIterator / FreeListView is a LegacyForwardIterator walking the implicit free list for diagnostics — counting free blocks, asserting ordering and integrity. Traversal is delegated to three gated C accessors (memory_pool_debug_free_list_head / _next / _free_count) that keep the ADR-0009 §1 layout encapsulated behind the Pimpl boundary. The entire surface is gated behind PBR_MEMORY_POOL_DIAGNOSTICS — on by default in debug builds (!NDEBUG), compiled out of release builds, and forced on by the PBR_MEMORY_POOL_ENABLE_DIAGNOSTICS CMake option. The allocation hot path is untouched in every configuration.
for (const void* slot : it::d4np::memorypool::FreeListView{pool}) { /* inspect */ }A dedicated container_integration CTest binary drives PoolAllocator<T> end-to-end through std::list (int + std::string, pool fast path, with diagnostics-gated free-count assertions), std::vector (heap fallback, growth / copy / <algorithm> interop), and a small hand-written allocator-aware ForwardList<T, Allocator> proving the adapter works with any conforming container. The pool-sizing recipe for node-based containers — block_size ≥ sizeof(rebound node), with safe degradation to the heap fallback when undersized — is documented as a worked example in the test header.
Four ADRs accepted in Milestone 3, taking the running total from 15 to 19:
- ADR-0016 — Exception policy at the C/C++ boundary (dual-verb
allocate/try_allocate, throwing ctor). - ADR-0017 —
TypedPool<T>design (compile-time block-size derivation, two-layer typed surface). - ADR-0018 — STL-compatible allocator Adapter (deterministic pool/fallback routing, propagation traits).
- ADR-0019 — Read-only free-list diagnostic Iterator (compile-time gating, C-encapsulated traversal).
Each ADR records 6–7 rejected alternatives so the decision rationale stays auditable from a single file.
Two patterns flip from Planned to Implemented in docs/patterns/README.md: Adapter (PoolAllocator<T>) and Iterator (FreeListIterator / FreeListView). Strategy, Template Method, Composite, Decorator, and Observer remain Planned against their respective Milestone 4–6 items.
No traceability row changes state in this release. Milestone 3 adds C++-side ergonomics over the already-✅ single-threaded core. 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 is unchanged from v0.2.0: eight ✅, one 🚧 (§6.3), and §2.2 / §2.4 / §6.3-concurrent in flight for Milestones 4–5.
- Thread safety — Milestone 4 →
v0.4.0. The implementation is single-threaded; concurrent use produces a data race. - Dynamic growth on exhaustion — Milestone 5 →
v0.5.0. Allocation surfaces exhaustion (NULLin C,std::bad_allocfromallocate) in fixed mode. - Instrumented / observable variants (Decorator, Observer, statistics, double-free detection) — Milestone 6 →
v0.6.0. - Doxygen-rendered API site, install / packaging layout (vcpkg, Conan) — Milestone 7 →
v1.0.0.
Each platform tarball produced by release.yml contains the public headers under include/it/d4np/memorypool/, the static archive under lib/, and the top-level LICENSE, README.md, CHANGELOG.md. SHA-256 checksums of every attached artifact live in SHA256SUMS:
sha256sum --check SHA256SUMSThe release pipeline re-runs the full CI matrix against the tagged commit — a green release workflow run is the canonical "the release is reproducible from a cold runner" signal.
cmake --preset release
cmake --build --preset release
ctest --preset releaseMinimal C++ consumer (type-safe pool):
#include <it/d4np/memorypool/typed_pool.hpp>
int main() {
it::d4np::memorypool::TypedPool<int> pool(1024);
int* p = pool.construct(42);
// ... use *p ...
pool.destroy(p);
}- Changelog entry:
CHANGELOG.md—[0.3.0] - Milestone plan:
ROADMAP.md— Milestone 3 - Specification:
docs/specs/01_spec_cpp_memory_pool.md - Previous release:
docs/releases/v0.2.0.md - Local Build Guide:
docs/development/local-build.md