Fifth tagged release of pbr-cpp-memory-pool. Milestone 5 adds optional, runtime-configurable dynamic growth (spec §2.2): a pool can be told to acquire a new contiguous chunk when exhausted, instead of failing. The default stays fixed-size — the v0.4.0 behaviour, bit-for-bit — so nothing changes for existing code.
A pool created with the new
memory_pool_t* memory_pool_create_dynamic(size_t block_size, size_t block_count, size_t growth_factor);grows geometrically on exhaustion: a new contiguous chunk supplies current_total × (growth_factor − 1) blocks, so the total capacity multiplies by the factor (default-style usage is factor 2 → doubling). The frozen memory_pool_create stays fixed-mode. On the C++ side, Pool::make_dynamic(block_size, block_count, growth_factor) mirrors Pool::make, and PoolBuilder gains .with_growth_factor(...):
auto pool = it::d4np::memorypool::PoolBuilder{}
.with_block_size(64).with_block_count(256).with_growth_factor(2)
.build(); // grows past 256 blocks on demandDynamic mode is a runtime, per-pool choice (not a compile-time knob): its decision point is the exhaustion slow path, so a non-exhausted pool never pays for it, and different pools in one program can choose differently.
Geometric, not linear — because the decisive metric is chunk count. Geometric growth keeps it O(log N); linear would make it O(N), degrading the two operations that walk the chunk list.
A Composite chunk list with one shared free list (ADR-0023)
The pool is now a Composite: its inline first chunk (the original backing_ / block_count_) plus an append-only forward-linked list of overflow Chunk leaves, threaded by one shared implicit free list. So:
memory_pool_alloc(pop) and thememory_pool_freepush stay O(1) across any chunk count;- only the
freeforeign-pointer check (ADR-0012) andmemory_pool_destroywalk the chunks — O(log N) in dynamic mode, O(1) in fixed mode; - per-block overhead stays zero — chunk descriptors are per-chunk (O(log N) total), never per-block;
- chunks are never moved — outstanding pointers stay valid (the whole point of a pool).
Growth runs inside the allocation pop_head hook under the active thread-safety policy's own synchronization (none for the single-threaded build, the held mutex for the mutex build); grow_pool is noexcept and falls back to fixed-mode exhaustion (NULL / std::bad_alloc) on OOM.
Lock-free + dynamic: rejected, by design (ADR-0024 §2)
In a library built with the lock-free thread-safety policy, memory_pool_create_dynamic (and Pool::make_dynamic / a growth PoolBuilder) returns NULL / std::nullopt: safe concurrent chunk-list growth needs atomic chunk links plus a grow-lock and cannot be ThreadSanitizer-verified, so it is deferred — like per-thread caches. Fixed-mode lock-free pools are fully supported; the rejection is an explicit contract, never a pool that silently never grows.
The dynamic-growth grow_factor_ field took the mutex-policy struct memory_pool to 136 bytes, so the ADR-0015 per-pool metadata budget is renegotiated 128 → 192 (per its §4). Per-block overhead remains zero; memory_pool_metadata_bytes now also sums the per-chunk overflow descriptors.
A dynamic_growth test binary covers repeated geometric growth, multiple factors, cross-chunk distinctness, full recovery / no-leak (ASan- and Valgrind-checked), the range check across grown chunks, and the lock-free rejection contract. The benchmark gains a growth scenario; on the maintainer's Skylake host a pool growing from 256 blocks is 1.96 × faster than malloc on amortized bulk allocation (vs ~11 × for a pre-sized fixed pool) — full numbers in docs/bench/v0.5.0-windows-msvc-x64-growth.md.
Three ADRs accepted in Milestone 5, taking the running total from 21 to 24:
- ADR-0022 — Dynamic-growth policy (geometric) and chunk-linking strategy.
- ADR-0023 — Composite chunk-list representation.
- ADR-0024 — Dynamic-growth synchronization, creation surface, and the lock-free deferral.
Composite flips to Implemented in docs/patterns/README.md. Decorator and Observer remain Planned against the Milestone 6 items.
§2.2 (O(1) allocation; return NULL / std::bad_alloc; dynamic growth optional) flips 🚧 → ✅ — both clauses are now satisfied. Coverage at the close of Milestone 5 is eleven rows ✅; the remaining work is the Milestone 6 observability items, which don't map to a distinct spec row.
- Instrumented / observable variants (Decorator, Observer, statistics, double-free detection) — Milestone 6 →
v0.6.0. - Lock-free dynamic growth — deferred (ADR-0024 §2); a future-milestone candidate.
- Pool shrink-on-idle — out of scope (ADR-0022); growth is monotonic.
- Doxygen-rendered API site, install / packaging (vcpkg, Conan) — Milestone 7 →
v1.0.0.
Each platform tarball produced by release.yml contains the public headers, the static archive, and LICENSE / README.md / CHANGELOG.md. SHA-256 checksums live in SHA256SUMS:
sha256sum --check SHA256SUMS#include <it/d4np/memorypool/memory_pool.hpp>
int main() {
using namespace it::d4np::memorypool;
// A growable pool: starts at 256 blocks, doubles on exhaustion.
if (auto pool = Pool::make_dynamic(64, 256, 2)) {
void* block = pool->try_allocate(); // grows transparently when needed
// ...
pool->deallocate(block);
}
}- Changelog entry:
CHANGELOG.md—[0.5.0] - Milestone plan:
ROADMAP.md— Milestone 5 - Specification:
docs/specs/01_spec_cpp_memory_pool.md - Growth benchmark:
docs/bench/v0.5.0-windows-msvc-x64-growth.md - Previous release:
docs/releases/v0.4.0.md