Compare commits

..
Author SHA1 Message Date
Niels Lohmann af394fca9c Share one nesting depth limit between all bounded descents
Copying and comparing stopped their descent at
basic_json::nesting_depth_limit(), while serializing, hashing and
merging used detail::recursion_depth_limit(). Both were 128, but
nothing kept them equal. The thread-local count now tests against
detail::recursion_depth_limit() as well, and a static_assert keeps
the limit small enough for the byte that holds the count.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 16:40:54 +02:00
6 changed files with 31 additions and 89 deletions
+1 -3
View File
@@ -195,7 +195,5 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Added in version 1.0.0.
2. Added in version 1.0.0.
3. Added in version 1.0.0.
4. Added in version 1.0.0. Fixed in version 3.13.0 to copy the values before inserting; before, an `ilist` that
referred to elements of the array being inserted into could insert wrong values, because the range insert could
move from or shift an element before it was copied.
4. Added in version 1.0.0.
5. Added in version 3.0.0.
@@ -19,11 +19,17 @@ namespace detail
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (serializing, hashing, merging, ...) recurse once
Operations that walk a value (copying, comparing, serializing, hashing, merging,
...) recurse once
per nesting level, which is fastest, but a value nested deeply enough would
exhaust the call stack. So they recurse only this many levels deep and finish
whatever lies below with an explicit stack. All of them share this limit.
Most of them pass the depth down as an argument. The copy constructor and the
comparison operators cannot, as their signatures are fixed, so they count it
in basic_json::nesting_depth() instead, a byte per thread; the limit must
therefore stay below 255.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
+8 -20
View File
@@ -898,12 +898,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#ifndef JSON_NO_THREAD_LOCAL
/// the number of levels an operation descends into before it finishes the
/// value below it without the call stack
static constexpr std::uint8_t nesting_depth_limit()
{
return 128;
}
// nesting_depth() is a byte and may exceed the limit by one level
static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte");
/*!
@brief how many levels the operation going on in this thread has descended into
@@ -945,7 +941,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static_cast<void>(may_descend);
return true;
#else
return !may_descend || nesting_depth() >= nesting_depth_limit();
return !may_descend || nesting_depth() >= detail::recursion_depth_limit();
#endif
}
@@ -969,7 +965,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
#ifdef JSON_NO_THREAD_LOCAL
: m_okay(false)
#else
: m_okay(nesting_depth() < nesting_depth_limit())
: m_okay(nesting_depth() < detail::recursion_depth_limit())
#endif
{
#ifndef JSON_NO_THREAD_LOCAL
@@ -1173,7 +1169,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
The values whose copy has not been created yet are kept on an explicit
worklist rather than on the call stack. This is only reached for values
nested deeper than @ref nesting_depth_limit levels, which is why it copies
nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies
every container by hand instead of letting the container do it: the fast
ways of doing so would descend into the elements and defeat the purpose.
*/
@@ -1239,7 +1235,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
Copying a container copies its elements, so a value nested deeply enough
used to exhaust the call stack. The descent is bounded here: the first
@ref nesting_depth_limit levels are copied by the containers themselves, just
@ref detail::recursion_depth_limit levels are copied by the containers themselves, just
as they always were, and anything below that is copied without the call
stack by @ref copy_iteratively. Copying a value can therefore no longer
exhaust the stack, however deeply it is nested, just like destroying one
@@ -1377,7 +1373,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 nesting_depth_limit levels, so
Reached once a comparison has descended @ref detail::recursion_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
@@ -4152,16 +4148,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this));
}
// copy the values first: ilist may refer to elements of this array
array_t values;
detail::reserve_array(values, ilist.size(), detail::priority_tag<1> {});
for (const auto& element : ilist)
{
values.push_back(element.moved_or_copied());
}
// insert to array and return iterator
return insert_iterator(pos, std::make_move_iterator(values.begin()), std::make_move_iterator(values.end()));
return insert_iterator(pos, ilist.begin(), ilist.end());
}
/// @brief inserts range of elements into object
+15 -21
View File
@@ -7281,11 +7281,17 @@ namespace detail
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (serializing, hashing, merging, ...) recurse once
Operations that walk a value (copying, comparing, serializing, hashing, merging,
...) recurse once
per nesting level, which is fastest, but a value nested deeply enough would
exhaust the call stack. So they recurse only this many levels deep and finish
whatever lies below with an explicit stack. All of them share this limit.
Most of them pass the depth down as an argument. The copy constructor and the
comparison operators cannot, as their signatures are fixed, so they count it
in basic_json::nesting_depth() instead, a byte per thread; the limit must
therefore stay below 255.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
@@ -26979,12 +26985,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#ifndef JSON_NO_THREAD_LOCAL
/// the number of levels an operation descends into before it finishes the
/// value below it without the call stack
static constexpr std::uint8_t nesting_depth_limit()
{
return 128;
}
// nesting_depth() is a byte and may exceed the limit by one level
static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte");
/*!
@brief how many levels the operation going on in this thread has descended into
@@ -27026,7 +27028,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static_cast<void>(may_descend);
return true;
#else
return !may_descend || nesting_depth() >= nesting_depth_limit();
return !may_descend || nesting_depth() >= detail::recursion_depth_limit();
#endif
}
@@ -27050,7 +27052,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
#ifdef JSON_NO_THREAD_LOCAL
: m_okay(false)
#else
: m_okay(nesting_depth() < nesting_depth_limit())
: m_okay(nesting_depth() < detail::recursion_depth_limit())
#endif
{
#ifndef JSON_NO_THREAD_LOCAL
@@ -27254,7 +27256,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
The values whose copy has not been created yet are kept on an explicit
worklist rather than on the call stack. This is only reached for values
nested deeper than @ref nesting_depth_limit levels, which is why it copies
nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies
every container by hand instead of letting the container do it: the fast
ways of doing so would descend into the elements and defeat the purpose.
*/
@@ -27320,7 +27322,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
Copying a container copies its elements, so a value nested deeply enough
used to exhaust the call stack. The descent is bounded here: the first
@ref nesting_depth_limit levels are copied by the containers themselves, just
@ref detail::recursion_depth_limit levels are copied by the containers themselves, just
as they always were, and anything below that is copied without the call
stack by @ref copy_iteratively. Copying a value can therefore no longer
exhaust the stack, however deeply it is nested, just like destroying one
@@ -27458,7 +27460,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 nesting_depth_limit levels, so
Reached once a comparison has descended @ref detail::recursion_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
@@ -30233,16 +30235,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this));
}
// copy the values first: ilist may refer to elements of this array
array_t values;
detail::reserve_array(values, ilist.size(), detail::priority_tag<1> {});
for (const auto& element : ilist)
{
values.push_back(element.moved_or_copied());
}
// insert to array and return iterator
return insert_iterator(pos, std::make_move_iterator(values.begin()), std::make_move_iterator(values.end()));
return insert_iterator(pos, ilist.begin(), ilist.end());
}
/// @brief inserts range of elements into object
-16
View File
@@ -117,22 +117,6 @@ TEST_CASE("array type without capacity()")
CHECK(nested.flatten().unflatten() == nested);
}
SECTION("insert(pos, initializer_list) compiles and works without reserve()")
{
// std::deque has no reserve() either; insert(pos, ilist) must not
// require it (regression test for #5656, which also covers an ilist
// that refers to elements of the array being inserted into)
deque_json j = deque_json::array();
j.push_back("a");
j.push_back("b");
j.push_back("c");
const deque_json& cj = j;
auto it = j.insert(j.begin(), {cj[0], cj[1]});
CHECK(*it == deque_json("a"));
CHECK(j == deque_json({"a", "b", "a", "b", "c"}));
}
SECTION("references stay valid while the array grows")
{
deque_json j = deque_json::array();
-28
View File
@@ -761,34 +761,6 @@ TEST_CASE("modifiers")
}
}
SECTION("initializer list referring to the array's own elements (#5656)")
{
SECTION("sufficient capacity (no reallocation)")
{
json j_own = json::array();
j_own.get_ref<json::array_t&>().reserve(8);
j_own.push_back("a");
j_own.push_back("b");
j_own.push_back("c");
const json& j_own_cref = j_own;
auto it = j_own.insert(j_own.begin(), {j_own_cref[0], j_own_cref[1]});
CHECK(*it == json("a"));
CHECK(j_own == json({"a", "b", "a", "b", "c"}));
}
SECTION("insufficient capacity (reallocation)")
{
json j_own = {"a", "b", "c"};
j_own.get_ref<json::array_t&>().shrink_to_fit();
const json& j_own_cref = j_own;
auto it = j_own.insert(j_own.begin(), {j_own_cref[2]});
CHECK(*it == json("c"));
CHECK(j_own == json({"c", "a", "b", "c"}));
}
}
SECTION("invalid iterator")
{
// pass iterator to a different array