Skip to content

Make basic_json destruction allocation-free and non-recursive - #5762

Merged
nlohmann merged 7 commits into
developfrom
fix/destroy-no-alloc-5135
Oct 6, 2026
Merged

nlohmann merged 7 commits into
developfrom
fix/destroy-no-alloc-5135

Conversation

@nlohmann

@nlohmann nlohmann commented Oct 4, 2026

Copy link
Copy Markdown
Owner

~basic_json() calls json_value::destroy(). To destroy deeply nested values without recursion, destroy() moved the children into a heap-allocated std::vector. This caused two problems:

This PR makes destroy() allocation-free while staying non-recursive. It walks the tree with pointer reversal:

  • cur is the container currently being emptied, and prev is the chain of its parents. While the walk is below a parent, that parent's last child slot stores the link to the parent's own parent.
  • A child is removed (pop_back() / erase() of the last element) only once it is a scalar or an empty container. Removing it therefore never recurses and never allocates.
  • Values move between the locals with a raw m_data transfer that skips set_parents(), which would cost O(children) per move with JSON_DIAGNOSTICS.

Without the stack, nothing in destroy() can throw. Each point from the review of #5239 is therefore resolved by construction:

  1. external_constructor can no longer end up with m_type and m_value out of sync, because destroy() never fails halfway.
  2. JSON_NOEXCEPTION needs no special handling, because there is no catch to compile away.
  3. No exception is swallowed, because none is thrown.
  4. Destruction does not recurse, even under memory pressure.

Destruction is also faster. Median of 30 runs, -O2, Apple clang, compared to develop:

Scenario develop this PR
array of 1M {"a":1,"b":[1,2,3],"c":"str"} 445.8 ms 135.2 ms 3.3× faster
object with 200k keys → small arrays 37.8 ms 18.7 ms 2.0× faster
100k-deep nested array 7.55 ms 2.45 ms 3.1× faster
first case with JSON_DIAGNOSTICS=1 692.2 ms 396.4 ms 1.75× faster

Tests:

This takes over two stale PRs. Their commits are included with the authors as git authors:

Note: the rewrite assumes that object_t has bidirectional iterators (std::prev(end())). This holds for std::map and ordered_map.

Closes #4842. Closes #5135. Supersedes #4843 and #5239. Thanks @veselink1 and @michaelsam94!

Public API

No breaking changes. destroy() is internal. The only observable differences: destroying a value never allocates and never calls std::terminate because an allocation failed.

🤖 Generated with Claude Code

@nlohmann nlohmann added the review needed It would be great if someone could review the proposed changes. label Oct 4, 2026
Comment thread include/nlohmann/json.hpp Outdated
{
return v.m_data.m_type == value_t::array
? v.m_data.m_value.array->back()
: std::prev(v.m_data.m_value.object->end())->second;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would rbegin() be better here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is only called once, just inline?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, last_child() now uses object->rbegin()->second (daad972). On inlining it: it is called on both cur and prev (ascending and descending), so I kept it as a helper.

(Written by Claude Code.)

Comment thread include/nlohmann/json.hpp
}
else
{
v.m_data.m_value.object->erase(std::prev(v.m_data.m_value.object->end()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rbegin as above?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I kept std::prev(end()): erase() needs a forward iterator, and std::next(rbegin()).base() would be harder to read. There is a comment saying so now.

(Written by Claude Code.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

Comment thread include/nlohmann/json.hpp
{
if (v.m_data.m_type == value_t::array)
{
AllocatorType<array_t> alloc;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use the allocator from the container instead of default constructing one?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this as is: create<T>() and the previous destroy() both default-construct AllocatorType<T>, so freeing matches allocation. Using a stateful allocator taken from the container would be a library-wide change (see #5298), not something to do only on the destruction side.

(Written by Claude Code.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I was thinking future proofing, and that it would always get the same allocator now, but I suppose that assumption could be wrong and it could trigger a new bug. Better to do it all at once.

Comment thread include/nlohmann/json.hpp
// set_parents()/assert_invariant() (the former is O(#children) per
// call under JSON_DIAGNOSTICS, which would make the walk below
// quadratic); dst takes ownership, src is left as value_t::null.
static void take(basic_json& dst, basic_json& src) noexcept

@gregmarr gregmarr Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function makes dangerous changes, should it be a lambda inside the destroy function?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept them as static member functions: several are used from more than one place, and this avoids lambda capture noise under C++11. They are only reachable from inside json_value, and the dangerous ones now assert their preconditions (empty container, expected type).

(Written by Claude Code.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be made private then?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 65a9dfe: the walk helpers (take, has_no_children, last_child, pop_last_child, free_container) are now private members of json_value. destroy_string/binary/container stay public since ~data() calls them.

(Written by Claude Code.)

Comment thread include/nlohmann/json.hpp Outdated
std::vector<basic_json> stack;

// move the top-level items to stack
// Destroy the tree without recursing per nesting level and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal style preference would be to have this after the switch, have the array and object cases check for null and break or return, have everything else do the null check, destroy, and return. That reduces a level of nesting, and has all of the simpler cases up front, also reduces the number of tests on t.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or see this comment which splits up the function: #5762 (comment)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in daad972, following your split suggestion: destroy() is now a flat switch with the simple cases first, dispatching to destroy_string(), destroy_binary() and destroy_container(t). Each of those does its own "not initialized" null check.

(Written by Claude Code.)

@gregmarr

gregmarr commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

I'm looking at json_value::destroy(). If I am reading this right, it is called from three locations. The first two are the erase() functions, which calls it for non-containers. It does nothing for anything other than string and binary. The other place is ~data().

I am wondering whether it would make sense to have destroy_string(), destroy_binary(), and destroy_container(value_t value) as more specific functions that would be easier to reason about.

~data()
{
  switch(m_type)
  {
    case string: destroy_string(); return;
    case binary: destroy_binary(); return;
    case object: case array: destroy_container(m_type); return;
    <all other cases here>
    default: return;
  }
}

I'm still a bit unsure why these member calls should set the json object to null:

    j_boolean.erase(j_boolean.begin());
    j_number_integer.erase(j_number_integer.begin());
    j_number_float.erase(j_number_float.begin());
    j_string.erase(j_string.begin());

Comment thread include/nlohmann/json.hpp Outdated
Comment on lines 710 to 720
basic_json cur;
cur.m_data.m_type = t;
cur.m_data.m_value = *this;
if (t == value_t::array)
{
stack.reserve(array->size());
std::move(array->begin(), array->end(), std::back_inserter(stack));
array = nullptr;
}
else
{
stack.reserve(object->size());
for (auto&& it : *object)
{
stack.push_back(std::move(it.second));
}
object = nullptr;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like

basic_json cur;
std::swap(cur.m_data, *this);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close. *this is the json_value, not the data, so it is now cur.m_data.m_type = t; swap(cur.m_data.m_value, *this);. A default-constructed value's union holds a null pointer, so *this ends up null exactly as before, and the if/else that cleared the pointer is gone.

(Written by Claude Code.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I shouldn't have taken off m_value. This would need to be in data for that swap to work.

Comment thread include/nlohmann/json.hpp Outdated
Comment on lines +748 to +750
const bool last_is_container = last.m_data.m_type == value_t::array || last.m_data.m_type == value_t::object;

if (!last_is_container || is_empty_container(last))

@gregmarr gregmarr Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this phrasing hard to reason about. Also, is_empty_container() requiring that we already know that it is a container isn't obvious.

bool has_no_children(basic_json &v)
{
  if (v.m_data.m_type == value_t::array) return v.m_data.m_value.array->empty());
  if (v.m_data.m_type == value_t::object) return v.m_data.m_value.object->empty());
  return true;
}

if (has_no_children(last))
{
  pop_last_child(cur);
  continue;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. is_empty_container() is replaced by has_no_children(), which returns true for non-containers too, so the loop now checks just if (has_no_children(cur_last_ref)) and the hidden "must already be a container" precondition is gone.

(Written by Claude Code.)

Comment thread include/nlohmann/json.hpp Outdated

// it's now safe that current_item gets destructed
// since it doesn't have any children
basic_json& last = last_child(cur);

@gregmarr gregmarr Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When working out a verification of the algorithm, I found it helpful to refer to this as cur.last& so maybe call it cur_last_ref?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to cur_last_ref.

(Written by Claude Code.)

Comment thread include/nlohmann/json.hpp Outdated
if (prev.m_data.m_type == value_t::null)
{
stack.push_back(std::move(it.second));
break; // back at the top with nothing left to do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the lines from after the loop here so no one has to look past the loop to see what happens next, and so that way we know that there are no other breaks from this loop.

if (prev.m_data.m_type == value_t::null)
{
  free_container(cur);
  return;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the "no children and no parent" case now does free_container(cur); return; inside the loop, so there is no break and nothing after the loop.

(Written by Claude Code.)

@gregmarr

gregmarr commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

It seems like the algorithm is sound. It might be useful to have tests for some of the potential edge cases. Containers with a mix of empty objects, empty arrays, non-empty objects, non-empty arrays, non-containers, containers in the first position, containers in the last position, both ordered and unordered objects.

Comment thread include/nlohmann/json.hpp
if (v.m_data.m_type == value_t::array)
{
AllocatorType<array_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert that the container is empty here and in the else?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added JSON_ASSERT(...->empty()) in both branches.

(Written by Claude Code.)

Comment thread include/nlohmann/json.hpp
}
else
{
AllocatorType<object_t> alloc;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert that type is object to catch mis-uses, especially if this is not a local lambda?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added JSON_ASSERT(v.m_data.m_type == value_t::object) in the object branches of free_container(), pop_last_child() and last_child().

(Written by Claude Code.)

@nlohmann

nlohmann commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the thorough review! daad972 addresses most of it; I replied to each inline comment.

I am wondering whether it would make sense to have destroy_string(), destroy_binary(), and destroy_container(value_t value)

Done, and the switch in destroy() now just dispatches to them.

I'm still a bit unsure why these member calls should set the json object to null

That is existing, documented behaviour of erase(iterator): "If called on a primitive type other than null, the resulting JSON value will be null." This PR only keeps it working: erase() still calls destroy() for strings and binary values as before.

It might be useful to have tests for some of the potential edge cases.

Added a TEST_CASE_TEMPLATE for json and ordered_json. It covers mixes of empty and non-empty arrays and objects with scalars (including string and binary), container children first only, last only, and both, single-element chains, top-level empty arrays and objects, an object → non-empty array → empty object tail, and destruction through erase() of a deeply nested child and through = nullptr on a deep tree. The "destructor performs no allocation" test also gained a mixed tree. Everything passes under ASan and UBSan, with and without JSON_DIAGNOSTICS.

(Written by Claude Code.)

@gregmarr

gregmarr commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

I'm still a bit unsure why these member calls should set the json object to null

That is existing, documented behaviour of erase(iterator): "If called on a primitive type other than null, the resulting JSON value will be null." This PR only keeps it working: erase() still calls destroy() for strings and binary values as before.

Yeah, just a general musing, not specific to this PR.

Comment thread include/nlohmann/json.hpp
// set_parents()/assert_invariant() (the former is O(#children) per
// call under JSON_DIAGNOSTICS, which would make the walk below
// quadratic); dst takes ownership, src is left as value_t::null.
static void take(basic_json& dst, basic_json& src) noexcept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be made private then?

Comment thread include/nlohmann/json.hpp
}
else
{
v.m_data.m_value.object->erase(std::prev(v.m_data.m_value.object->end()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

Comment thread include/nlohmann/json.hpp
{
if (v.m_data.m_type == value_t::array)
{
AllocatorType<array_t> alloc;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I was thinking future proofing, and that it would always get the same allocator now, but I suppose that assumption could be wrong and it could trigger a new bug. Better to do it all at once.

Comment thread include/nlohmann/json.hpp
}
}

void destroy(value_t t)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of putting this body in ~data because ~data only calls destroy and destroy is only called from ~data. It also means that you don't have another function called destroy() which is different than the allocator_traits::destroy() that it calls. I see that I didn't explain my rationale and didn't explicitly call that out. Just a suggestion, fine with keeping it as is if that's your preference.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this, but destroy() isn't only called from ~data(). The 19 external_constructor<...>::construct overloads in to_json.hpp and both erase() overloads also call m_value.destroy(m_type), so it has to stay a shared dispatcher. I'd rather not duplicate the switch in all those places.

(Written by Claude Code.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I didn't find most of those calls. Thanks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what I did. I was only searching in the PR and not in the whole code, and thus only saw the two calls in erase(), which could be replaced with destroy_string() and destroy_binary(), and the call in ~data().

veselink1 and others added 6 commits October 6, 2026 07:25
Uses the provided allocator to allocate the stack used to avoid
recursion in the destroy() implementation used by ~basic_json.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Adds a regression test for #4842: destroying a nested array or object must allocate its temporary stack through the basic_json allocator, not std::allocator.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Michael Sam <michaelsam94@users.noreply.github.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
destroy() used to flatten a nested array/object into a heap-allocated
std::vector to avoid recursing per nesting level. That vector could
itself throw bad_alloc under memory pressure, and since it now used the
basic_json's own allocator (#4842), a failing allocator supplied by the
caller made this more likely, not less. An exception thrown from inside
~basic_json(), which is noexcept, terminates the program (#5135).

Replace the vector-based stack with a pointer-reversal walk that visits
the tree without recursing per level and without allocating anything:
cur is the array/object currently being emptied, prev is its parent
(or null at the top). A parent's last child slot doubles as storage for
that parent's own parent link while we are below it, so no extra memory
is needed. A child is only ever removed once it is a scalar or an empty
array/object, which neither allocates nor recurses more than one level
deep. take() moves m_data between these locals directly, bypassing
set_parents()/assert_invariant() (the former is O(#children) per call
under JSON_DIAGNOSTICS, which would make the walk quadratic otherwise).

This also removes the std::vector<basic_json, allocator_type> stack
added by #4842, so the extra allocations it introduced disappear along
with it.

Co-authored-by: Michael Sam <9461037+michaelsam94@users.noreply.github.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Update the #4842 regression test: it used to check that destroying a
nested array/object made at least one allocation through the provided
allocator (the old flattening stack). Now that destroy() does not
allocate at all, assert the opposite: zero allocations, deallocations
only.

Rework the #5135 regression test to use a dedicated failing/counting
allocator instead of overriding the process-wide ::operator new and
::operator delete, which affected every allocation in the whole
unit-regression2 binary rather than just the values under test. Keep
the original small repro as one case, and add deep (100000 levels) and
wide-and-deep nested array/object/ordered_json cases, all destroyed
while every further allocation is made to fail: the destructor must
complete without allocating, without throwing, and without leaking.

Co-authored-by: Michael Sam <9461037+michaelsam94@users.noreply.github.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Apply review feedback from Greg Marr on the json_value::destroy()
non-recursive, allocation-free destruction walk (#5135):

- last_child() now uses object->rbegin()->second instead of
  std::prev(object->end())->second; pop_last_child() keeps
  std::prev(end()) since erase() needs a forward iterator.
- is_empty_container() becomes has_no_children(), a switch that
  returns true for every non-container type as well as empty
  array/object, simplifying the "scalar or already-empty child"
  check at the call site. The local variable `last` is renamed to
  `cur_last_ref` for clarity.
- free_container() asserts the array/object is already empty before
  freeing it, and the object branches assert the expected type.
- destroy(value_t t) is now a thin dispatcher to destroy_string(),
  destroy_binary(), and destroy_container(t), each handling its own
  "not initialized" check and sharing the simple cases first in the
  switch.
- destroy_container() moves the top-level container into the local
  stand-in via a plain swap of the json_value union, instead of a
  manual copy plus clearing array/object by hand.
- The "cur has no children and there is no parent" case now frees
  cur and returns immediately, so the main loop is a plain
  while (true) with no trailing code after it.

Also adds edge-case tests for both json and ordered_json (mixes of
empty/non-empty arrays and objects, container children in first/last
position, single-element chains, top-level empty containers, and
destruction via erase()/assignment), plus a mixed-tree case in the
"destructor performs no allocation" test.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the fix/destroy-no-alloc-5135 branch from daad972 to 230c460 Compare October 6, 2026 05:26
They modify basic_json internals without maintaining its invariants and
are only meant for destroy_container(), so they no longer need to be
reachable from the rest of basic_json.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann removed the review needed It would be great if someone could review the proposed changes. label Oct 6, 2026
@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 6, 2026
@nlohmann
nlohmann merged commit 0a36586 into develop Oct 6, 2026
4 of 164 checks passed
@nlohmann
nlohmann deleted the fix/destroy-no-alloc-5135 branch October 6, 2026 05:40
nlohmann added a commit that referenced this pull request Oct 6, 2026
The non-recursive destroy walk from #5762 picked an object's last child
via object_t::rbegin() and std::prev(end()). Neither is available for
every ObjectType: no_key_compare_map in unit-custom-object-type.cpp has
no rbegin(), so develop no longer compiles that test, and hash maps such
as std::unordered_map only have forward iterators.

The walk can take an object's children in any order, as long as it
finds the same child again while the object is not modified in between.
So objects with bidirectional iterators keep using their last child
(O(1) to remove from vector-based maps like ordered_map), and objects
with forward-only iterators use begin() instead. No reverse iteration
or rbegin() is needed any more, and the walk stays allocation-free.

Adds a forward-only ObjectType to the tests, destroyed both with mixed
nesting and 100000 levels deep.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann mentioned this pull request Oct 7, 2026
2 tasks done
nlohmann added a commit that referenced this pull request Oct 7, 2026
- unit-regression2.cpp (#5762): const/auto for the destroy() test values;
  NOLINT the intended copy in check_destroy_edge_case().
- unit-serialization.cpp (#5774): build the expected strings with += instead
  of chained operator+ (performance-inefficient-string-concatenation).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 7, 2026
* Fix CI on develop after #5585

- test-diagnostics-optimized: -O3 makes GCC's -Winline and
  -Wsuggest-attribute=pure/const warnings fire with the ci_test_gcc flag
  set; turn them off for this test.
- test-diagnostics-optimized: suppress Clang's -Wexit-time-destructors for
  the static table in to_json.
- Infer: raise pulse-max-disjuncts from 20 to 40. With the default,
  Pulse loses the stored type in basic_json::replace_value() and reports
  false null dereferences of get_ptr() results in unit-pointer_access.cpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Ignore Infer's false USE_AFTER_DELETE in ordered_map::erase

Infer's std::string model keeps the buffer of a moved-from string, so the
destroy-and-reconstruct loop in erase(first, last) looks like it destroys a
buffer twice.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Mark throw_on_discarded()'s parameters as used without exceptions

With JSON_NOEXCEPTION, JSON_THROW expands to std::abort(), so Clang's
-Wunused-parameter breaks test-disabled_exceptions (since #5761).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Skip the span_input_adapter sax_parse checks with deleted deprecated functions

The #5676 regression test (#5740) calls the deprecated
sax_parse(span_input_adapter&&, ...), which JSON_DELETE_DEPRECATED_FUNCTIONS
deletes, so ci_test_delete_deprecated_functions failed to build.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fall back to the first entry in test-diagnostics-optimized's to_json

clang-tidy (clang-analyzer-security.ArrayBound) flagged it->second for a
value not in the table. Use the same fallback as
NLOHMANN_JSON_SERIALIZE_ENUM; the test still fails with -Werror=array-bounds
on the headers from before #5585.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix clang-tidy findings in tests from #5762 and #5774

- unit-regression2.cpp (#5762): const/auto for the destroy() test values;
  NOLINT the intended copy in check_destroy_edge_case().
- unit-serialization.cpp (#5774): build the expected strings with += instead
  of chained operator+ (performance-inefficient-string-concatenation).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 16, 2026
Copy timed only the copy and destroyed it with the timer paused, and
Destroy did the reverse. How a document is freed shapes the heap the next
copy allocates from: since the faster destruction (#5762/#5767), part of
its saved work shows up in the next copy's allocations. Copy therefore
looked up to 40% slower than 3.12.0 on glibc although copying and
destroying together got 1.1-1.5x faster.

Copy now times a temporary copy including its destruction. CopyAssign
assigns over an earlier copy, and CopyRing replaces the oldest of eight
live copies. The old benchmarks remain as CopyOnly and DestroyOnly, with
a note that they tell the halves apart but do not compare versions.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

basic_json destructor allocates memory, violating noexcept semantics json destructor does not use the provided allocator

4 participants