Idea: arena / PMR document mode (O(1) teardown, allocation-free destruction)
Status: brainstorming, not a plan
This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss. Treat this as a starting point for discussion.
Motivation
This is really the general cure for a cluster of already-tracked symptoms, reframed as one design rather than a new feature:
- #5135 — destructor allocates, violating
noexceptsemantics - #3583 —
basic_jsondestructor is slow - #5239 — allocation failure during JSON destruction
- #4843 — use the provided allocator in
destroy()
All four have the same root cause: there is no arena, so teardown is a recursive walk that frees every node individually. With a monotonic arena, teardown is O(1) and allocation-free by construction.
Why PMR is blocked today — a one-line diagnosis
create<T>() at include/nlohmann/json.hpp:411 does AllocatorType<T> alloc; — it default-constructs a fresh allocator on every allocation, and the destroy sites (include/nlohmann/json.hpp:652+, :2526+, :2597+) do the same. That hard-codes the assumption that the allocator is stateless.
std::pmr::polymorphic_allocator is stateful — it holds a memory_resource*. Under the current code every node would default-construct a pmr allocator pointing at the default resource (ignoring the intended arena), and teardown would deallocate through a default-constructed allocator, which for pmr is undefined behavior (deallocating on the wrong resource). No allocator instance is stored anywhere in basic_json, and nothing propagates one to children.
Two ways forward
(a) Full stateful-allocator support — probably not worth it
Store the allocator in each node, honor allocator_traits POCCA/POCMA/select_on_container_copy_construction, propagate on copy/move. "Correct," but deeply invasive and it grows sizeof(basic_json) by a pointer per node (the union is currently ~8 bytes + a type byte; +8 bytes everywhere is a real regression for large trees).
(b) Arena / PMR document mode — the Boost.JSON model (recommended)
- Store the memory resource once at the root (a
storage_ptr-style handle), not per node. - Thread it through
parse()so all of a document's nodes come from onestd::pmr::monotonic_buffer_resource. - Crucially, tear the document down by dropping the arena, not by running per-node destructors. This needs a "no individual free" destruction path (skip the deallocate loop when the tree is arena-owned) — the part that's missing today.
This directly resolves #5135 / #3583 / #5239 / #4843: teardown becomes O(1) and cannot allocate or throw.
Sketch
std::pmr::monotonic_buffer_resource arena;
auto doc = json::parse_into(arena, src); // illustrative: all nodes from `arena`
// ... use doc ...
// destruction: drop `arena`; no per-node frees run
Implementation notes
- Add a
storage_ptr-like handle (non-owning or ref-counted pointer to amemory_resource) stored once at the document root; children reference it. - Route
create<T>()and every destroy site through the document's resource instead of a default-constructed allocator. These sites are load-bearing and must all be updated consistently. - Add an arena-owned flag / teardown path that skips per-node deallocation.
- Keep the change opt-in: default behavior and
sizeof(basic_json)must be unchanged for existing users.
Open questions / pitfalls
- Copying an arena-backed value out of its document must deep-copy into the destination's allocator — arena-owned storage cannot be shared across trees.
select_on_container_copy_construction-correct behavior at the document boundary, even if individual nodes stay allocator-light.- Move semantics across documents with different resources.
- How the public API expresses "this document owns/references arena X" without leaking lifetime footguns (the arena must outlive the document).
- Interaction with
JSON_NO_IO/binary paths and with the borrowed-view idea (#5295), which is a different way to avoid per-node allocation (references source bytes rather than arena-allocating owned nodes).
Dependencies / interactions
- Subsumes the destructor-cost issues #5135, #3583, #5239, #4843 — best treated as their shared design rather than four separate fixes.
- Independent of the lossless-number (#5296) and canonical-serialization (#5297) ideas.
- Complementary to, but distinct from, the borrowed-view idea (#5295): arena mode still produces an owning, mutable DOM; the view does not.
Source: nlohmann/json