From da25f77b72ffb60731834e007d158548ef4dc08f Mon Sep 17 00:00:00 2001 From: Daniel Polo <106583643+danielPoloWork@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:00:54 +0200 Subject: [PATCH 1/2] feat(api): add a std::pmr::memory_resource adapter (PoolMemoryResource) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PoolMemoryResource binds one Pool behind the runtime std::pmr::memory_resource interface, so a single resource can back any std::pmr container via std::pmr::polymorphic_allocator without the per-type PoolAllocator rebind — the "door left open" in ADR-0018. Deterministic (bytes, alignment) routing to the bound pool: fitting requests come from the pool (throwing std::bad_alloc on exhaustion, never falling back, so deallocate routing stays deterministic); over-sized / over-aligned requests delegate to a configurable upstream resource (get_default_resource() by default). is_equal compares (pool, upstream) identity. The whole facility is gated behind PBR_MEMORY_POOL_HAS_PMR (a __cpp_lib_memory_resource feature-test) so the header is a harmless no-op where std::pmr is unavailable. Header-only and purely additive: the frozen C ABI and the existing C++ types are unchanged, so this is a SemVer MINOR. It opens roadmap Milestone 9 (post-1.0 ergonomics, hardening & tooling; ADR-0037). - ADR-0042 records the decision (+ ADR index row). - Dedicated doctest (pool_memory_resource), gated like the header. - Spec §5.2 lists the type; §7 maps it to ADR-0042; §7.1 un-defers it. - ROADMAP Milestone 9 created with this as item 9.1. - Doxyfile predefines __cpp_lib_memory_resource so the gated class is documented on the API site. - CHANGELOG [Unreleased] Added entry. The README milestone-table row and the patterns-catalogue refinement are deferred to the v1.2.0 release PR to keep this feature PR off the translated docs surface (they would trip the i18n-freshness gate). Validated locally: MSVC build + doctest (5 cases / 33 assertions), clang-format and clang-tidy clean, consistency lint OK. Closes #107 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 11 ++ ROADMAP.md | 9 + docs/adr/0042-pmr-memory-resource-adapter.md | 165 ++++++++++++++++ docs/adr/README.md | 1 + docs/doxygen/Doxyfile | 1 + docs/specs/01_spec_cpp_memory_pool.md | 3 +- .../d4np/memorypool/pool_memory_resource.hpp | 178 +++++++++++++++++ .../cpp/it/d4np/memorypool/CMakeLists.txt | 23 +++ .../memorypool/pool_memory_resource_test.cpp | 179 ++++++++++++++++++ 9 files changed, 569 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0042-pmr-memory-resource-adapter.md create mode 100644 src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp create mode 100644 src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b44d5d..bf212b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,17 @@ dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a mil ### Added +- **`std::pmr::memory_resource` adapter — `PoolMemoryResource`.** A new header-only Adapter + ([`pool_memory_resource.hpp`](src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp)) + binds one `Pool` behind the runtime `std::pmr::memory_resource` interface, so a single + resource can back any `std::pmr` container through `std::pmr::polymorphic_allocator` without + the per-type `PoolAllocator` rebind — the "door left open" in ADR-0018. Deterministic + `(bytes, alignment)` routing to the pool (over-sized / over-aligned requests fall back to a + configurable upstream resource; exhaustion throws `std::bad_alloc`), `is_equal` by + `(pool, upstream)` identity, gated behind `PBR_MEMORY_POOL_HAS_PMR` where `` + is available. Purely additive and ABI-compatible — the frozen C ABI and existing C++ types + are unchanged; opens roadmap Milestone 9 (a `v1.2.0` candidate). + [ADR-0042](docs/adr/0042-pmr-memory-resource-adapter.md). Closes #107. - **C4 component diagram of the pool internals** in the specification ([`docs/specs/01_spec_cpp_memory_pool.md`](docs/specs/01_spec_cpp_memory_pool.md), §4.2), authored in Mermaid. [ADR-0041](docs/adr/0041-mermaid-diagram-tooling.md) records Mermaid diff --git a/ROADMAP.md b/ROADMAP.md index 4212f6b..a1a8ffe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,15 @@ Goal: *post-`v1.0.0`*, stand up a **modular, professional documentation-translat - [x] 8.8 **Close Milestone 8 → `v1.1.0`**: bump `version.hpp` (`MINOR=1`), roll `CHANGELOG.md` `Unreleased` into a `[1.1.0]` block, draft `docs/releases/v1.1.0.md`, refresh the README status block / badge / milestone table, and open the release PR (ADR-0004 §2). First post-1.0 minor — the §8.5 protocol governs from here on. [`version.hpp`](src/main/cpp/it/d4np/memorypool/version.hpp) is bumped to `MAJOR=1 MINOR=1 PATCH=0` and `PBR_MEMORY_POOL_VERSION_STRING` to `"1.1.0"`; the `pool_smoke` version-check `TEST_CASE` asserts the new components. [`CHANGELOG.md`](CHANGELOG.md) `[Unreleased]` is rolled into a sealed `## [1.1.0] — 2026-06-14` block (the M8.1–M8.7 + M8.9 subsections under a Milestone 8 headline; first post-1.0 MINOR, purely additive docs/governance — library binary unchanged); fresh empty `[Unreleased]`; bottom link references rewritten (`[Unreleased]` → `compare/v1.1.0...HEAD`, new `[1.1.0]` → `releases/tag/v1.1.0`). [`docs/releases/v1.1.0.md`](docs/releases/v1.1.0.md) carries the human-prose release notes for `release.yml`. [`README.md`](README.md) status badge (`v1.0.1` → `v1.1.0`), status paragraph, and milestone table (Milestone 8 `⏳ next` → `✅ complete`) are refreshed. The closing release classified as a **MINOR** per the M8.5 decision tree (additive surface, no API/ABI/behaviour change). The maintainer reviews and merges this PR, then the agent tags `v1.1.0` from `master` per [ADR-0008](docs/adr/0008-delegate-tag-creation-and-push-to-the-agent.md), and the maintainer clicks *Publish* on the draft GitHub Release. **This closes Milestone 8 and the planned roadmap.** - [x] 8.9 *(Emerged mid-M8, prerequisite for §8.3/§8.4)* Establish **English as the specification's normative language**. The spec [`docs/specs/01_spec_cpp_memory_pool.md`](docs/specs/01_spec_cpp_memory_pool.md) was authored in Italian (the original contract); [ADR-0032](docs/adr/0032-documentation-i18n-architecture.md) assumes a single English-normative source and `AGENTS.md` §2 mandates English for on-disk artifacts, so the spec cannot be coherently localized (the "English is normative" banner) while its source is Italian. Decided in [ADR-0033](docs/adr/0033-english-as-the-spec-normative-language.md): the spec is translated to English **in place** as the normative source — a **faithful** translation (every requirement, the API, the Free List description, the diagram, and the verification strategy preserved with identical meaning; only the prose language changes, so the frozen contract is not semantically altered), with the Italian original preserved in git history (commit `3ccff68`). Three alternatives rejected (keep Italian + translate per-language, add `it` as an i18n target, dual-language file). This unblocks the §8.3 / §8.4 spec translations from a clean English source. `documentation.md` gains a "spec is maintained in English" note. +## Milestone 9 — Ergonomics, Hardening & Tooling + +Goal: a coherent, post-`v1.0.0` wave of **additive, ABI-compatible** capabilities on top of the frozen public surface — richer standard-library interop, opt-in memory-safety hardening, a fuzzing harness, and a broader benchmark baseline. Every item is additive, so the milestone targets `v1.2.0` (SemVer `MINOR`); each ships independently under the §6.1 one-PR-at-a-time rule, and closing the milestone is the `v1.2.0` bump ([ADR-0037](docs/adr/0037-new-feature-roadmap-placement.md)). Each item comes from a tracking issue. + +- [x] 9.1 **`std::pmr::memory_resource` adapter** (`PoolMemoryResource`) — the "door left open" in [ADR-0018](docs/adr/0018-stl-allocator-adapter.md): a `std::pmr::memory_resource` subclass binding one `Pool` so any `std::pmr`-aware container can draw from it through `std::pmr::polymorphic_allocator`, without the `PoolAllocator` per-type rebind. Deterministic `(bytes, alignment)` routing to the bound pool — over-sized / over-aligned requests delegate to a configurable upstream resource, and exhaustion of a pool-eligible request throws `std::bad_alloc` rather than falling back (preserving the deterministic deallocate routing) — with `is_equal` by `(pool, upstream)` identity, gated behind `PBR_MEMORY_POOL_HAS_PMR` where `` is available. Header-only, additive, ABI-compatible. Decided in [ADR-0042](docs/adr/0042-pmr-memory-resource-adapter.md); implemented in [`pool_memory_resource.hpp`](src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp) with [`pool_memory_resource_test.cpp`](src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp) (issue #107). +- [ ] 9.2 **Opt-in debug hardening** — freed-block poisoning, canaries, and free-list safe-linking (which also yields double-free detection); zero cost when the gate is off (issue #109). +- [ ] 9.3 **Coverage-guided fuzzing harness** for the pool surface — a libFuzzer target under `src/fuzz/`, time-boxed in CI (issue #108). +- [ ] 9.4 **Benchmark extension** — external allocator baselines (jemalloc / tcmalloc) and p99 tail-latency reporting (issue #111). + --- ## Spec Coverage Map diff --git a/docs/adr/0042-pmr-memory-resource-adapter.md b/docs/adr/0042-pmr-memory-resource-adapter.md new file mode 100644 index 0000000..f1707a0 --- /dev/null +++ b/docs/adr/0042-pmr-memory-resource-adapter.md @@ -0,0 +1,165 @@ +# ADR-0042: `std::pmr::memory_resource` Adapter — `PoolMemoryResource` + +- **Status:** Accepted +- **Date:** 2026-07-07 +- **Deciders:** Daniel Polo (maintainer / project architect) +- **Related:** [ADR-0018](0018-stl-allocator-adapter.md) (the STL allocator Adapter whose alternatives explicitly left this "door open"), [ADR-0009](0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md) §2/§5 (the fixed `block_size` and `alignof(std::max_align_t)` guarantee bounding which requests the pool can serve), [ADR-0010](0010-raii-pool-wrapper-and-pimpl-across-the-c-cpp-boundary.md) (the `Pool` the adapter references), [ADR-0016](0016-exception-policy-at-the-c-cpp-boundary.md) §2 (the `std::bad_alloc`-on-exhaustion verb the pool path forwards to), [ADR-0037](0037-new-feature-roadmap-placement.md) (this feature opens roadmap Milestone 9), issue #107, spec [§5.2](../specs/01_spec_cpp_memory_pool.md#52-c17-surface) + +## Context + +[ADR-0018](0018-stl-allocator-adapter.md) added `PoolAllocator`, the compile-time +*Cpp17Allocator* Adapter. Its alternatives section recorded one option as *deferred, not +rejected*: a `std::pmr::memory_resource` subclass, which "would let one pool back many +`pmr` containers without the template rebind dance … recorded here so the door stays open." +Roadmap Milestone 9 (post-1.0 ergonomics; [ADR-0037](0037-new-feature-roadmap-placement.md)) +opens that door. + +`PoolAllocator` is a *type* — `std::list>` bakes the pool into the +container's type, and every distinct element type needs its own rebound specialisation. The +`std::pmr` model is different and complementary: containers are spelled `std::pmr::list` +(their allocator is the type-erased `std::pmr::polymorphic_allocator`), and the storage +policy is chosen at **run time** by handing them a `std::pmr::memory_resource*`. One resource +object can therefore back heterogeneous containers (`std::pmr::list`, `std::pmr::vector`, +`std::pmr::string`, …) without any rebind ceremony. + +Four forces shape the design, mirroring ADR-0018's four questions adapted to `pmr`: + +1. **What does the resource own?** A `memory_resource` is used through a pointer and copied + into every `polymorphic_allocator`; the move-only `Pool` (ADR-0010) cannot be its member. +2. **What is the request shape?** `do_allocate(bytes, alignment)` — a single contiguous + region with an explicit alignment, and **no element count**. The fit test is therefore in + bytes, not in `(n, sizeof(T))`. +3. **How does `do_deallocate` route without per-pointer bookkeeping?** +4. **How is portability handled**, given `std::pmr` shipped unevenly across standard + libraries? + +## Decision + +We add a header-only Adapter, +`it::d4np::memorypool::PoolMemoryResource : public std::pmr::memory_resource`, in +[`pool_memory_resource.hpp`](../../src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp). + +1. **Non-owning back-reference.** It holds a `Pool*` **and** an upstream + `std::pmr::memory_resource*`. The pool and the upstream are owned elsewhere and must + out-live the resource and every `polymorphic_allocator` / container bound to it — the same + lifetime contract `polymorphic_allocator` places on its resource, and `PoolAllocator` + on its `Pool`. + +2. **Deterministic size/alignment routing — no per-pointer bookkeeping.** A request routes to + the pool **iff** one block can satisfy it: + + ```cpp + bytes <= pool.block_size() && alignment <= alignof(std::max_align_t) + ``` + + Pool-eligible requests are served by `Pool::try_allocate`, throwing `std::bad_alloc` on + exhaustion (ADR-0016 §2). Every other request (over-sized or over-aligned) is delegated to + the **upstream** resource. Because `do_deallocate` receives the same `(bytes, alignment)` + the matching `do_allocate` got, and `block_size()` is invariant for the pool's lifetime, + the predicate evaluates identically at allocate and deallocate — every pointer is freed by + exactly the path that produced it, with zero bookkeeping. This is the same insight that + makes `PoolAllocator` correct (ADR-0018 §2), re-expressed in `pmr`'s byte/alignment + interface. + +3. **Configurable upstream.** The constructor takes + `std::pmr::memory_resource* upstream = std::pmr::get_default_resource()`, so the resource + composes into a `pmr` chain like the standard pool resources — and a test can inject + `std::pmr::null_memory_resource()` to prove a request was routed upstream (it throws) + rather than served by the pool. + +4. **Equality by identity.** `do_is_equal` is `true` iff `other` is also a + `PoolMemoryResource` bound to the **same** pool and the **same** upstream, so a + `polymorphic_allocator` may deallocate through either. + +5. **Availability gate.** The whole facility is compiled only when + `__cpp_lib_memory_resource >= 201603L` (detected after a `__has_include()` + probe) and exposed through `PBR_MEMORY_POOL_HAS_PMR`. Where `std::pmr` is absent the header + is a harmless no-op — the same "gate the optional surface" posture as the diagnostics gate + (ADR-0019). + +The adapter is header-only and purely additive: no change to the frozen C ABI or the existing +C++ types, so this is a SemVer **MINOR** (a `v1.2.0` candidate). + +## Alternatives Considered + +- **Do nothing — tell users to wrap `PoolAllocator` themselves.** Rejected. It leaves the + ADR-0018 door shut: `std::pmr` containers cannot use the pool at all without a resource, and + hand-rolling one per project is exactly the reusable component this library should provide. +- **Fall back to the upstream on pool *exhaustion* too.** Rejected, for the same reason + ADR-0018 rejected it for `PoolAllocator`: a pool-eligible pointer could then have come from + either the pool or the upstream, so `do_deallocate` could no longer decide the path from + `(bytes, alignment)` alone — forcing a side table or an ownership probe on every free. + Exhaustion of a pool-eligible request therefore throws `std::bad_alloc` (ADR-0016 §2). +- **Owning resource (hold the `Pool` by value).** Rejected. `Pool` is move-only and a + `memory_resource` is referenced/pointed-to, not copied by value into a container; the + non-owning back-reference is the only shape that fits the `pmr` model, and it matches + `PoolAllocator`. +- **No upstream parameter — hardcode `get_default_resource()` (or global `operator new`).** + Rejected. An explicit, defaulted upstream is idiomatic `pmr` (resources compose into + chains), costs nothing at the call site, and makes the routing testable by injecting + `null_memory_resource`. +- **Route in `do_deallocate` by asking the pool "is this pointer mine?"** Rejected. There is + no public ownership predicate, and `memory_pool_free` treats a foreign pointer as a silent + no-op (ADR-0012) — which would leak an upstream pointer of a pool-eligible size. The + deterministic size route is the only coherent option. +- **Assume C++17 conformance; don't gate on availability.** Rejected. libc++ shipped + `` well after C++17, and freestanding profiles omit it; a feature-test gate + keeps the header portable and the CI matrix (ADR-0005) green everywhere. + +## Consequences + +**Positive** + +- A single `PoolMemoryResource` backs any number of heterogeneous `std::pmr` containers and + any `polymorphic_allocator`, with no per-type rebind — the ergonomic win ADR-0018 foresaw. +- Deterministic routing gives correct `do_deallocate` with zero per-pointer overhead; the + adapter adds only a couple of compile-time-foldable comparisons and one `block_size()` load + on the pool path. +- Header-only and additive: zero library-ABI impact and zero metadata-budget impact + (ADR-0015 untouched). It is a clean, documented **MINOR**. + +**Negative** + +- `std::pmr` dispatches through a virtual call on every (de)allocation. That cost is inherent + to the `pmr` model and is precisely why `PoolAllocator` (inlineable, non-virtual) remains + the zero-overhead default; `PoolMemoryResource` is the *ergonomics* option, chosen with eyes + open. +- As with `PoolAllocator`, a single container can hold storage from two sources over its + lifetime (pool for fitting node allocations, upstream for over-sized buffers). Correct, but + worth documenting. +- The non-owning back-reference puts a hard lifetime contract on the caller: the pool **and** + the upstream must out-live the resource and everything bound to it. + +**Testing / tooling / documentation (landing in the same PR)** + +- [`pool_memory_resource_test.cpp`](../../src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp) + — a dedicated doctest binary (CTest `pool_memory_resource`), gated like the test file so it + builds even where `std::pmr` is absent; it runs under the existing ASan/UBSan/TSan matrix. +- [`docs/specs/01_spec_cpp_memory_pool.md`](../specs/01_spec_cpp_memory_pool.md) — §5.2 lists + `PoolMemoryResource`; §7 maps it to this ADR; §7.1 no longer defers it. (The spec is a + translated source but is already `stale` from the prior spec PRs, so this edit adds no new + i18n debt.) +- [`ROADMAP.md`](../../ROADMAP.md) — Milestone 9 is created (ADR-0037) with this as item 9.1. +- **Deferred to the `v1.2.0` release PR** to keep this feature PR off the *translated* docs + surface (editing them trips the `i18n-freshness` gate): the + [`docs/patterns/README.md`](../patterns/README.md) **Adapter** row refinement — the Adapter + pattern is already catalogued (row 5, ADR-0018) and `PoolMemoryResource` is a second + application of it, fully documented here and in the spec §7 map — and the `README.md` + Milestone-9 table row (a release-time refresh per the M8.8 pattern). The release PR refreshes + both and re-syncs the `zh-Hans` / `ja` translations in one pass. +- [`docs/doxygen/Doxyfile`](doxygen/Doxyfile) `PREDEFINED` gains + `__cpp_lib_memory_resource=201603L` so the gated class is documented on the API site. +- [`CHANGELOG.md`](../../CHANGELOG.md) `Unreleased` — an *Added* entry. + +## References + +- ISO C++17 [mem.res.class] — the `std::pmr::memory_resource` interface (`do_allocate`, + `do_deallocate`, `do_is_equal`). +- ISO C++17 [mem.poly.allocator.class] — `std::pmr::polymorphic_allocator` and its resource + lifetime contract. +- [ADR-0018](0018-stl-allocator-adapter.md) — the sibling `PoolAllocator` Adapter and the + deterministic-routing rationale reused here. +- [ADR-0016](0016-exception-policy-at-the-c-cpp-boundary.md) §2 — the `std::bad_alloc` + exhaustion verb the pool path forwards to. +- cppreference — `std::pmr::memory_resource`, `std::pmr::polymorphic_allocator`, + `std::pmr::null_memory_resource`, `std::pmr::get_default_resource`. diff --git a/docs/adr/README.md b/docs/adr/README.md index 7ce8d0c..1f86548 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -74,5 +74,6 @@ Do **not** write one for purely local implementation details, formatting, or tri | 0039 | [In-repo bug ledger and agent triage protocol](0039-bug-ledger-and-triage-protocol.md) | Accepted | | 0040 | [Pull-request metadata policy (assignee, labels, milestone)](0040-pull-request-metadata-policy.md) | Accepted | | 0041 | [Mermaid as the in-repo diagram tooling](0041-mermaid-diagram-tooling.md) | Accepted | +| 0042 | [`std::pmr::memory_resource` adapter — `PoolMemoryResource`](0042-pmr-memory-resource-adapter.md) | Accepted | When adding a new ADR, append a row to this table in the same PR. diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index b3ad30d..debd797 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -66,6 +66,7 @@ EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = YES INCLUDE_PATH = src/main/cpp PREDEFINED = PBR_MEMORY_POOL_DIAGNOSTICS=1 \ + __cpp_lib_memory_resource=201603L \ __cplusplus=201703L #--------------------------------------------------------------------------- diff --git a/docs/specs/01_spec_cpp_memory_pool.md b/docs/specs/01_spec_cpp_memory_pool.md index 4fccce4..d7e5909 100644 --- a/docs/specs/01_spec_cpp_memory_pool.md +++ b/docs/specs/01_spec_cpp_memory_pool.md @@ -169,6 +169,7 @@ A layered, idiomatic C++ surface built on the C core: - `Pool` — move-only RAII owner; `PoolBuilder` / factory methods for construction ([ADR-0010](../adr/0010-raii-pool-wrapper-and-pimpl-across-the-c-cpp-boundary.md), [ADR-0011](../adr/0011-factory-method-and-builder-for-pool-construction.md)). - `TypedPool` — type-safe `construct`/`destroy` over the pool ([ADR-0017](../adr/0017-typed-pool-design.md)). - `PoolAllocator` — an STL `Allocator` adapter so the pool can back `std::list`, `std::map`, … ([ADR-0018](../adr/0018-stl-allocator-adapter.md)). +- `PoolMemoryResource` — a `std::pmr::memory_resource` adapter so one pool can back any `std::pmr` container via `std::pmr::polymorphic_allocator`, without the per-type rebind. Header-only and gated behind `PBR_MEMORY_POOL_HAS_PMR` (compiled where `` is available) ([ADR-0042](../adr/0042-pmr-memory-resource-adapter.md)). - `InstrumentedPool` — a Decorator adding counters and lifecycle Observers ([ADR-0025](../adr/0025-decorator-for-instrumented-pool.md), [ADR-0026](../adr/0026-observer-for-pool-lifecycle-events.md)). ### 5.3 Error semantics @@ -221,6 +222,7 @@ Every requirement above is realized and recorded. The table maps the spec to its | §4 free-list layout, constraints, alignment, intrusive-vs-bitmap | [ADR-0009](../adr/0009-free-list-layout-block-size-constraints-and-alignment-guarantee.md) | | §4.2 component (C4) diagram & diagram tooling | [ADR-0041](../adr/0041-mermaid-diagram-tooling.md) | | §5.1–5.2 API, RAII, Pimpl, builder, typed pool, STL adapter | [ADR-0010](../adr/0010-raii-pool-wrapper-and-pimpl-across-the-c-cpp-boundary.md), [ADR-0011](../adr/0011-factory-method-and-builder-for-pool-construction.md), [ADR-0017](../adr/0017-typed-pool-design.md), [ADR-0018](../adr/0018-stl-allocator-adapter.md) | +| §5.2 `std::pmr` adapter (PoolMemoryResource) | [ADR-0042](../adr/0042-pmr-memory-resource-adapter.md) | | §5.3 error semantics | [ADR-0012](../adr/0012-foreign-pointer-and-out-of-range-pointer-policy.md), [ADR-0016](../adr/0016-exception-policy-at-the-c-cpp-boundary.md) | | §5.4 instrumentation / observers | [ADR-0025](../adr/0025-decorator-for-instrumented-pool.md), [ADR-0026](../adr/0026-observer-for-pool-lifecycle-events.md) | | §6.3 benchmark methodology | [ADR-0014](../adr/0014-microbenchmark-methodology-pool-vs-malloc.md) | @@ -230,7 +232,6 @@ Every requirement above is realized and recorded. The table maps the spec to its These are explicitly out of the current build and tracked as issues: -- **`std::pmr::memory_resource` adapter** — the "door left open" in [ADR-0018](../adr/0018-stl-allocator-adapter.md) (issue #107). - **Coverage-guided fuzzing harness** (issue #108). - **Opt-in debug hardening** — freed-block poisoning, canaries, free-list safe-linking; would also add double-free detection (issue #109). - **Benchmark extension** — external baselines (jemalloc/tcmalloc) and p99 percentile reporting (issue #111). diff --git a/src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp b/src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp new file mode 100644 index 0000000..1672253 --- /dev/null +++ b/src/main/cpp/it/d4np/memorypool/pool_memory_resource.hpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Daniel Polo + +#ifndef IT_D4NP_MEMORYPOOL_POOL_MEMORY_RESOURCE_HPP_ +#define IT_D4NP_MEMORYPOOL_POOL_MEMORY_RESOURCE_HPP_ + +/** + * @file pool_memory_resource.hpp + * @brief `std::pmr::memory_resource` Adapter over `Pool` — ADR-0042. + * + * `PoolMemoryResource` is the second **Adapter** the library ships (the + * sibling of the `PoolAllocator` Adapter in ADR-0018). Where + * `PoolAllocator` bridges the pool to the compile-time + * `std::allocator_traits` contract — one rebound allocator type per element + * type — `PoolMemoryResource` bridges the pool to the **runtime** + * `std::pmr::memory_resource` interface, so a *single* resource can back any + * number of heterogeneous `std::pmr` containers through + * `std::pmr::polymorphic_allocator` without the per-type rebind dance. This is + * exactly the "door left open" in ADR-0018's alternatives. + * + * The adapter is a **non-owning** back-reference: it holds a `Pool*` and an + * upstream `std::pmr::memory_resource*`. The pool (and the upstream) must + * out-live this resource and every `polymorphic_allocator` / container bound + * to it — the same lifetime contract `std::pmr::polymorphic_allocator` places + * on its resource, and `PoolAllocator` on its `Pool`. + * + * Header-only; adds zero object code to the static library and zero per-pool + * metadata (ADR-0015 unaffected). + * + * **Availability.** `std::pmr` is a C++17 facility, but some standard + * libraries shipped `` late and freestanding profiles omit + * it. The whole adapter is therefore gated behind `PBR_MEMORY_POOL_HAS_PMR`, + * which is `1` only when `` and its + * `__cpp_lib_memory_resource` feature-test macro are present. A consumer can + * test that macro before relying on the type; where it is `0` this header is a + * harmless no-op. + */ + +#include + +#if defined(__has_include) +#if __has_include() +#include +#endif +#endif + +// Availability gate (see the @file note). This drives `#if` here, in the +// dedicated test, and in consumer code, so it must be a preprocessor macro +// rather than a constexpr constant — hence the macro-usage suppression. +/* NOLINTBEGIN(cppcoreguidelines-macro-usage) */ +#if defined(__cpp_lib_memory_resource) && __cpp_lib_memory_resource >= 201603L +#define PBR_MEMORY_POOL_HAS_PMR 1 +#else +#define PBR_MEMORY_POOL_HAS_PMR 0 +#endif +/* NOLINTEND(cppcoreguidelines-macro-usage) */ + +#if PBR_MEMORY_POOL_HAS_PMR + +#include +#include + +namespace it::d4np::memorypool { + +/** + * @brief A `std::pmr::memory_resource` that draws single blocks from a `Pool`. + * + * **Routing** mirrors ADR-0018 §2, adapted to `std::pmr`'s + * `(bytes, alignment)` interface (there is no element count `n` here — a + * `memory_resource` request is a single contiguous region). A request routes + * to the pool **iff** one pool block can satisfy it: + * + * @code + * bytes <= pool.block_size() && alignment <= alignof(std::max_align_t) + * @endcode + * + * - **Pool-eligible:** served by `Pool::try_allocate`, throwing + * `std::bad_alloc` on exhaustion (ADR-0016 §2). It **never** falls back to + * the upstream on exhaustion — doing so would break the deterministic + * routing below (see ADR-0042). + * - **Everything else** (over-sized or over-aligned): delegated to the + * `upstream` resource (`std::pmr::get_default_resource()` by default). + * + * Because `do_deallocate` receives the *same* `(bytes, alignment)` the + * matching `do_allocate` received, and `block_size()` is invariant for the + * pool's lifetime, the routing predicate evaluates identically at allocate and + * deallocate time. Every pointer is therefore released through exactly the + * path that produced it, with **zero per-pointer bookkeeping** — the same + * insight that makes `PoolAllocator` correct. + * + * Two resources compare equal (`is_equal`) iff they are both + * `PoolMemoryResource` bound to the same `Pool` **and** the same upstream, so + * a `polymorphic_allocator` can safely deallocate through either. + */ +class PoolMemoryResource : public std::pmr::memory_resource { +public: + /** + * Bind the resource to @p pool, delegating requests the pool cannot serve + * to @p upstream. Neither the pool nor the upstream is owned; both must + * out-live this resource and everything bound to it. + * + * @param pool The fixed-block pool that serves pool-eligible requests. + * @param upstream Resource for over-sized / over-aligned requests; defaults + * to `std::pmr::get_default_resource()`. + */ + explicit PoolMemoryResource(Pool& pool, + std::pmr::memory_resource* upstream = std::pmr::get_default_resource()) noexcept + : pool_(&pool), upstream_(upstream) {} + + /** @return The bound pool (non-owning). */ + [[nodiscard]] Pool& pool() const noexcept { + return *pool_; + } + + /** @return The upstream resource requests fall back to (non-owning). */ + [[nodiscard]] std::pmr::memory_resource* upstream_resource() const noexcept { + return upstream_; + } + +private: + /** + * Serve one allocation (ADR-0042). Pool-eligible requests come from the + * pool (throwing `std::bad_alloc` on exhaustion); all others go upstream. + * + * @throw std::bad_alloc on pool exhaustion, or whatever the upstream + * resource throws. + */ + // The `(bytes, alignment)` pair is the std::pmr::memory_resource override + // signature and cannot be restructured into a parameter struct. + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + void* do_allocate(std::size_t bytes, std::size_t alignment) override { + if (routes_to_pool(bytes, alignment)) { + void* const block = pool_->try_allocate(); + if (block == nullptr) { + throw std::bad_alloc{}; + } + return block; + } + return upstream_->allocate(bytes, alignment); + } + + /** + * Release storage obtained from ::do_allocate. @p bytes and @p alignment + * must match the originating request (a `memory_resource` guarantee), + * which keeps the pool / upstream routing deterministic. + */ + // Fixed override signature — see ::do_allocate. + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + void do_deallocate(void* ptr, std::size_t bytes, std::size_t alignment) override { + if (routes_to_pool(bytes, alignment)) { + pool_->deallocate(ptr); + return; + } + upstream_->deallocate(ptr, bytes, alignment); + } + + /** Equal iff same concrete type bound to the same pool and upstream. */ + [[nodiscard]] bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { + const auto* const rhs = dynamic_cast(&other); + return rhs != nullptr && rhs->pool_ == pool_ && rhs->upstream_ == upstream_; + } + + /** ADR-0042 routing predicate — pure in (bytes, alignment, block_size). */ + // Mirrors the std::pmr `(bytes, alignment)` shape — see ::do_allocate. + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + [[nodiscard]] bool routes_to_pool(std::size_t bytes, std::size_t alignment) const noexcept { + return bytes <= pool_->block_size() && alignment <= alignof(std::max_align_t); + } + + Pool* pool_; + std::pmr::memory_resource* upstream_; +}; + +} // namespace it::d4np::memorypool + +#endif // PBR_MEMORY_POOL_HAS_PMR + +#endif // IT_D4NP_MEMORYPOOL_POOL_MEMORY_RESOURCE_HPP_ diff --git a/src/test/cpp/it/d4np/memorypool/CMakeLists.txt b/src/test/cpp/it/d4np/memorypool/CMakeLists.txt index 475a812..ab55a8c 100644 --- a/src/test/cpp/it/d4np/memorypool/CMakeLists.txt +++ b/src/test/cpp/it/d4np/memorypool/CMakeLists.txt @@ -199,6 +199,29 @@ add_test( set_tests_properties(zero_overhead PROPERTIES LABELS "smoke;milestone-6") +# ROADMAP item 9.1 — the std::pmr::memory_resource Adapter (ADR-0042). The +# adapter is header-only and gated behind PBR_MEMORY_POOL_HAS_PMR (availability +# of ); the test TU compiles in both states (a placeholder +# case when the surface is gated out), so this binary builds in every +# configuration and is the compile-time proof that pool_memory_resource.hpp +# instantiates cleanly (including through std::pmr::list / std::pmr::vector) +# under the full warning set. +add_executable(pool_memory_resource_test + pool_memory_resource_test.cpp) + +target_link_libraries(pool_memory_resource_test PRIVATE + pbr::memory_pool + doctest::doctest) + +target_compile_features(pool_memory_resource_test PRIVATE cxx_std_17) + +add_test( + NAME pool_memory_resource + COMMAND pool_memory_resource_test) + +set_tests_properties(pool_memory_resource PROPERTIES + LABELS "smoke;milestone-9") + # ROADMAP item 2.8 — spec §6.2 Valgrind demonstrative test. The directory # carries its own CMakeLists.txt because the target consumes a strict ANSI # C89 .c source and overrides the project-wide C99 floor on its own scope. diff --git a/src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp b/src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp new file mode 100644 index 0000000..cb9fb8e --- /dev/null +++ b/src/test/cpp/it/d4np/memorypool/pool_memory_resource_test.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Daniel Polo + +/** + * @file pool_memory_resource_test.cpp + * @brief Tests for the `PoolMemoryResource` std::pmr Adapter (M9.1 / ADR-0042). + * + * The suite is gated behind `PBR_MEMORY_POOL_HAS_PMR` so the binary still + * builds (with a single placeholder case) on a toolchain whose standard + * library does not provide ``. When enabled, the cases prove + * that: + * - pool-eligible requests (fit one block) are served from the pool and + * exhaust it — so they are demonstrably *not* silently upstream-backed; + * - deallocation returns a block to the pool (a freed slot is reissued); + * - over-sized and over-aligned requests route to the upstream resource, not + * the pool (proven with `null_memory_resource`, whose allocate always + * throws, and by leaving the pool undrained); + * - `is_equal` is by (pool, upstream) identity, across resource types; + * - a real `std::pmr` container and a `polymorphic_allocator` round-trip + * end-to-end through the resource. + */ + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#include + +#include + +#if PBR_MEMORY_POOL_HAS_PMR + +#include + +#include +#include +#include +#include +#include + +using it::d4np::memorypool::Pool; +using it::d4np::memorypool::PoolMemoryResource; + +namespace { + +// A block size comfortably above any small pmr node on a 64-bit host and a +// multiple of alignof(max_align_t) (ADR-0009 §2) so Pool construction succeeds. +constexpr std::size_t BLOCK_SIZE = 64U; +constexpr std::size_t BLOCK_COUNT = 8U; + +} // namespace + +TEST_CASE("PoolMemoryResource serves pool-eligible requests from the pool and exhausts it") { + Pool pool(BLOCK_SIZE, BLOCK_COUNT); + PoolMemoryResource mr(pool); + + // Draining exactly BLOCK_COUNT fitting allocations then seeing the next + // throw proves the storage came from the fixed-capacity pool, not from an + // unbounded upstream fallback. + std::vector blocks; + for (std::size_t i = 0; i < BLOCK_COUNT; ++i) { + void* const p = mr.allocate(BLOCK_SIZE); + REQUIRE(p != nullptr); + blocks.push_back(p); + } + CHECK_THROWS_AS(static_cast(mr.allocate(BLOCK_SIZE)), std::bad_alloc); + + // Returning one block and re-allocating succeeds — the slot went back to + // the pool's free list (deterministic routing, ADR-0042). + mr.deallocate(blocks.back(), BLOCK_SIZE); + blocks.pop_back(); + void* const reissued = mr.allocate(BLOCK_SIZE); + CHECK(reissued != nullptr); + blocks.push_back(reissued); + + for (void* const p : blocks) { + mr.deallocate(p, BLOCK_SIZE); + } +} + +TEST_CASE("PoolMemoryResource routes over-sized and over-aligned requests to the upstream") { + Pool pool(BLOCK_SIZE, BLOCK_COUNT); + // A null upstream throws on every request it receives, so a throw proves + // the request was routed upstream rather than served by the pool. + PoolMemoryResource mr(pool, std::pmr::null_memory_resource()); + + // Over-sized (two blocks' worth) → upstream (null) → throws. + CHECK_THROWS_AS(static_cast(mr.allocate(BLOCK_SIZE * 2U)), std::bad_alloc); + // Over-aligned (beyond max_align_t) → upstream (null) → throws. + CHECK_THROWS_AS(static_cast(mr.allocate(BLOCK_SIZE, alignof(std::max_align_t) * 2U)), std::bad_alloc); + + // A pool-eligible request still succeeds — it never touched the null + // upstream — and the pool was left fully available by the routed-away + // requests above (BLOCK_COUNT fitting allocations all succeed). + std::vector blocks; + for (std::size_t i = 0; i < BLOCK_COUNT; ++i) { + void* const p = mr.allocate(BLOCK_SIZE); + REQUIRE(p != nullptr); + blocks.push_back(p); + } + for (void* const p : blocks) { + mr.deallocate(p, BLOCK_SIZE); + } +} + +TEST_CASE("PoolMemoryResource::is_equal compares (pool, upstream) identity across types") { + Pool pool_a(BLOCK_SIZE, BLOCK_COUNT); + Pool pool_b(BLOCK_SIZE, BLOCK_COUNT); + + PoolMemoryResource ra1(pool_a); + PoolMemoryResource ra2(pool_a); + PoolMemoryResource rb(pool_b); + PoolMemoryResource ra_upstream(pool_a, std::pmr::null_memory_resource()); + + // Equal iff same pool AND same upstream (the free operator== consults + // is_equal after the identity short-circuit). + CHECK(ra1 == ra2); + CHECK(ra1 != rb); + CHECK(ra1 != ra_upstream); + + // Never equal to a different concrete resource type. + CHECK(ra1 != *std::pmr::new_delete_resource()); + CHECK_FALSE(ra1.is_equal(*std::pmr::new_delete_resource())); +} + +TEST_CASE("std::pmr::list round-trips through PoolMemoryResource") { + Pool pool(BLOCK_SIZE, BLOCK_COUNT); + PoolMemoryResource mr(pool); + + // A node-based container allocates one node at a time; whether each node + // fits a pool block or spills to the upstream, the container stays correct. + std::pmr::list values(&mr); + for (int i = 0; i < 5; ++i) { + values.push_back(i); + } + REQUIRE(values.size() == 5U); + + int sum = 0; + for (const int v : values) { + sum += v; + } + CHECK(sum == 0 + 1 + 2 + 3 + 4); + + values.clear(); + CHECK(values.empty()); +} + +TEST_CASE("std::pmr::vector and a polymorphic_allocator round-trip through PoolMemoryResource") { + Pool pool(BLOCK_SIZE, BLOCK_COUNT); + PoolMemoryResource mr(pool); + + // vector requests contiguous n>1 storage as it grows; the resource serves + // that from the upstream while staying correct. + std::pmr::vector values(&mr); + for (int i = 0; i < 100; ++i) { + values.push_back(i); + } + REQUIRE(values.size() == 100U); + CHECK(values.front() == 0); + CHECK(values.back() == 99); + + // A polymorphic_allocator built from the resource vends single blocks from + // the pool fast path and hands them back. + std::pmr::polymorphic_allocator alloc(&mr); + int* const one = alloc.allocate(1); + REQUIRE(one != nullptr); + *one = 42; + CHECK(*one == 42); + alloc.deallocate(one, 1); +} + +#else // PBR_MEMORY_POOL_HAS_PMR + +TEST_CASE("std::pmr adapter is compiled out on this toolchain") { + // The standard library does not provide ; the adapter is + // gated out (ADR-0042). This placeholder keeps the binary and its CTest + // registration valid, mirroring the free_list_iterator_test pattern. + CHECK(true); +} + +#endif // PBR_MEMORY_POOL_HAS_PMR From 8401d5e3d6604ee3e835c37386f570843902e153 Mon Sep 17 00:00:00 2001 From: Daniel Polo <106583643+danielPoloWork@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:03:26 +0200 Subject: [PATCH 2/2] docs(adr): fix the relative Doxyfile link in ADR-0042 The link resolved to docs/adr/doxygen/Doxyfile (from the ADR directory) and tripped the Lychee internal-link check; correct it to ../doxygen/Doxyfile. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/adr/0042-pmr-memory-resource-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0042-pmr-memory-resource-adapter.md b/docs/adr/0042-pmr-memory-resource-adapter.md index f1707a0..4f7adbd 100644 --- a/docs/adr/0042-pmr-memory-resource-adapter.md +++ b/docs/adr/0042-pmr-memory-resource-adapter.md @@ -147,7 +147,7 @@ C++ types, so this is a SemVer **MINOR** (a `v1.2.0` candidate). application of it, fully documented here and in the spec §7 map — and the `README.md` Milestone-9 table row (a release-time refresh per the M8.8 pattern). The release PR refreshes both and re-syncs the `zh-Hans` / `ja` translations in one pass. -- [`docs/doxygen/Doxyfile`](doxygen/Doxyfile) `PREDEFINED` gains +- [`docs/doxygen/Doxyfile`](../doxygen/Doxyfile) `PREDEFINED` gains `__cpp_lib_memory_resource=201603L` so the gated class is documented on the API site. - [`CHANGELOG.md`](../../CHANGELOG.md) `Unreleased` — an *Added* entry.