Use the shared descent bookkeeping rather than a second set

Comparing kept a thread_local count, a limit and a guard of its own beside
the ones copying already had, all three the same thing under a different
name. They are gone; the shared count, limit and guard do the work.

The guard grows a second constructor here, because the comparison
operators are written as a macro and a macro cannot use the preprocessor:
it cannot look the count up behind an #ifdef the way copy_structured does,
so the guard looks it up for it. nesting_depth_exhausted() arrives for the
same reason - whether an operator descends at all is a constant at every
call site, and testing it there is what MSVC reports as C4127.

Also say in compare_leaves what happens to a pair that is an array on one
side and an object on the other, since the answer is not obvious from the
code: an operator only descends into two values of the same type, so such
a pair is told apart by its types alone - unequal, and ordered the way the
types are - exactly as it is above the bound.

And record what the explicit stack costs: the comparison operators are
noexcept and the container comparison this replaces allocated nothing, so
running out of memory here ends the process instead of throwing. It takes
a value nested past the bound and an exhausted heap to reach, and the same
comparison used to exhaust the call stack, but it is a new way to fail.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-08-21 12:30:56 +02:00
parent 5fe4cd288a
commit 596e33c319
2 changed files with 140 additions and 148 deletions
+70 -74
View File
@@ -22200,35 +22200,61 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
return depth;
}
/// @brief how many levels the comparison going on in this thread has descended into
static std::size_t& compare_depth() noexcept
{
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
return depth;
}
#endif
/*!
@brief whether a descent must stop here and finish without the call stack
@a may_descend says whether the operator descends at all; it is a constant
at every call site, and is passed rather than tested by the caller so that
the test does not become a constant condition there, which MSVC reports as
C4127.
*/
static bool nesting_depth_exhausted(bool may_descend = true) noexcept
{
#ifdef JSON_NO_THREAD_LOCAL
// without a count of its own per thread, a descent cannot be bounded
// without racing another one, so none is made
static_cast<void>(may_descend);
return true;
#else
return !may_descend || nesting_depth() >= nesting_depth_limit();
#endif
}
/*!
@brief counts one level of a bounded descent for as long as it runs
The count is taken rather than looked up here, because the caller has looked
it up already to test it against the limit: reaching thread-local storage is
not free, and the path that is taken almost every time should reach it once
rather than twice.
The constructor taking the count is for callers that have looked it up
already to test it: reaching thread-local storage is not free, and the path
that is taken almost every time should reach it once rather than twice. The
other is for callers that cannot look it up - the comparison operators are
written as a macro, and a macro cannot use the preprocessor.
*/
class nesting_depth_guard
{
public:
explicit nesting_depth_guard(std::size_t& depth) noexcept
: m_depth(depth)
: m_depth(&depth)
{
++m_depth;
++*m_depth;
}
nesting_depth_guard() noexcept
: m_depth(countable())
{
if (m_depth != nullptr)
{
++*m_depth;
}
}
~nesting_depth_guard()
{
--m_depth;
if (m_depth != nullptr)
{
--*m_depth;
}
}
nesting_depth_guard(const nesting_depth_guard&) = delete;
@@ -22237,7 +22263,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
nesting_depth_guard& operator=(nesting_depth_guard&&) = delete;
private:
std::size_t& m_depth;
/// @brief the count to keep, or nullptr where there is none to keep
static std::size_t* countable() noexcept
{
#ifdef JSON_NO_THREAD_LOCAL
return nullptr;
#else
return &nesting_depth();
#endif
}
std::size_t* m_depth;
};
/// an entry of the iterative deep copy's worklist: a structured value and
@@ -22516,53 +22552,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// ordered at all, such as a discarded value or a NaN
enum class compare_result { less, equal, greater, unordered };
/*!
@brief counts one level of a comparison for as long as it runs
Does nothing without thread_local storage, where no descent is made at all.
*/
class compare_depth_guard
{
public:
compare_depth_guard() noexcept
{
#ifndef JSON_NO_THREAD_LOCAL
++compare_depth();
#endif
}
~compare_depth_guard() noexcept
{
#ifndef JSON_NO_THREAD_LOCAL
--compare_depth();
#endif
}
compare_depth_guard(const compare_depth_guard&) = delete;
compare_depth_guard& operator=(const compare_depth_guard&) = delete;
compare_depth_guard(compare_depth_guard&&) = delete;
compare_depth_guard& operator=(compare_depth_guard&&) = delete;
};
/*!
@brief whether a comparison must stop descending and finish iteratively
@a may_descend says whether the operator descends at all; it is constant at
every call site, and is passed rather than tested by the caller so that the
test does not become a constant condition there, which MSVC reports (C4127).
*/
static bool compare_descent_exhausted(bool may_descend) noexcept
{
#ifdef JSON_NO_THREAD_LOCAL
// without a counter of its own per thread, the descent cannot be
// bounded without racing another one, so none is made
static_cast<void>(may_descend);
return true;
#else
return !may_descend || compare_depth() >= compare_depth_limit();
#endif
}
#if JSON_HAS_THREE_WAY_COMPARISON
/// @brief the ordering that @a result stands for
static std::partial_ordering to_partial_ordering(compare_result result) noexcept // *NOPAD*
@@ -22582,18 +22571,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#endif
/// the number of levels a comparison descends into before it compares what
/// is left without the call stack
static constexpr std::size_t compare_depth_limit()
{
return 128;
}
/*!
@brief compare two values that are not both an array or both an object
Such a pair is compared by the operators themselves, which cannot descend
into it and therefore cannot recurse.
That holds for a pair whose types differ as much as for a pair of leaves: an
array and an object are told apart by their types alone, because an operator
only ever descends into two values of the same type. So `==` reports them as
unequal without looking inside either, and an ordering falls back to the
order of the types - an object sorts before an array - exactly as it does
for a value that is not nested deeply enough to get here.
*/
template<bool Ordered>
static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept
@@ -22667,7 +22656,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief compare @a lhs and @a rhs without descending into them
Reached once a comparison has descended @ref compare_depth_limit levels, so
Reached once a comparison has descended @ref nesting_depth_limit levels, so
that comparing values cannot exhaust the call stack however deeply they are
nested. The two values are walked in lockstep on an explicit stack and
compared lexicographically, element by element in the order the containers
@@ -22676,6 +22665,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
nlohmann::ordered_map in insertion order. An object type that enumerates its
entries in an unspecified order, such as std::unordered_map, compares them
pairwise instead; the difference could only ever show below the bound.
Note that the stack this walks with is allocated, while the comparison
operators are noexcept and the container comparison this replaces allocated
nothing. Failing that allocation therefore ends the process rather than
throwing. It only arises for values nested past the bound, and only when
memory has run out - where the same comparison used to exhaust the call
stack instead - but it is a way to fail that the operators did not have.
*/
template<bool Ordered>
static compare_result compare_iteratively(const_reference lhs, const_reference rhs,
@@ -25601,21 +25597,21 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{ \
case value_t::array: \
{ \
if (JSON_HEDLEY_UNLIKELY(compare_descent_exhausted(may_descend))) \
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
{ \
return (deep_result); \
} \
const compare_depth_guard guard; \
const nesting_depth_guard guard; \
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
} \
\
case value_t::object: \
{ \
if (JSON_HEDLEY_UNLIKELY(compare_descent_exhausted(may_descend))) \
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
{ \
return (deep_result); \
} \
const compare_depth_guard guard; \
const nesting_depth_guard guard; \
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
} \
\