Skip to content

Latest commit

 

History

History
119 lines (79 loc) · 9.38 KB

File metadata and controls

119 lines (79 loc) · 9.38 KB

pbr-cpp-memory-pool v0.3.0 — Milestone 3: C++ Wrapper & Type Safety

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.

What's in the box

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 throws std::bad_alloc on exhaustion (and on a moved-from wrapper).
  • The new Pool::try_allocate() is noexcept and returns nullptr — the exact v0.2.0 allocate() semantics, preserved under a new name.
  • The Pool(block_size, block_count) constructor now throws std::bad_alloc when memory_pool_create fails, retiring the ADR-0010 silent-empty-state. Pool::make / PoolBuilder::build remain 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() returning nullptr on exhaustion should migrate to try_allocate(); code relying on the silent-empty-state constructor should use Pool::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 slot

PoolAllocator<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 */ }

Container integration suite (M3.5)

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.

Architecture Decision Records

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-0017TypedPool<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.

Design-patterns catalogue

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.

Spec Coverage Map

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.

What this release does not contain

  • 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 (NULL in C, std::bad_alloc from allocate) 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.

Verifying the release

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 SHA256SUMS

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

Build and use

cmake --preset release
cmake --build --preset release
ctest --preset release

Minimal 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);
}

Links