Share one recursion depth limit, and copy the hash frame out of the stack

dump() and hash() each defined their own limit on how many nesting levels
they recurse into, and the operations still to come would have added more,
free to diverge over time. They now all use detail::recursion_depth_limit(),
in a header of its own; serializer::dump_depth_limit() and
hash_depth_limit() are gone.

hash_iteratively() now copies the frame it works on out of the stack and
changes the frame only through stack.back(), so nothing can refer into
the stack after entering an element has grown it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-23 23:07:11 +02:00
parent 75c61f91f1
commit 57a5a5d48e
7 changed files with 113 additions and 58 deletions
+1
View File
@@ -55,6 +55,7 @@ cc_library(
"include/nlohmann/detail/output/binary_writer.hpp", "include/nlohmann/detail/output/binary_writer.hpp",
"include/nlohmann/detail/output/output_adapters.hpp", "include/nlohmann/detail/output/output_adapters.hpp",
"include/nlohmann/detail/output/serializer.hpp", "include/nlohmann/detail/output/serializer.hpp",
"include/nlohmann/detail/recursion_depth_limit.hpp",
"include/nlohmann/detail/string_concat.hpp", "include/nlohmann/detail/string_concat.hpp",
"include/nlohmann/detail/string_escape.hpp", "include/nlohmann/detail/string_escape.hpp",
"include/nlohmann/detail/string_utils.hpp", "include/nlohmann/detail/string_utils.hpp",
+12 -17
View File
@@ -14,6 +14,7 @@
#include <vector> // vector #include <vector> // vector
#include <nlohmann/detail/abi_macros.hpp> #include <nlohmann/detail/abi_macros.hpp>
#include <nlohmann/detail/recursion_depth_limit.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN NLOHMANN_JSON_NAMESPACE_BEGIN
@@ -27,13 +28,6 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
return seed; return seed;
} }
/// the number of levels @ref hash descends into before handing over to
/// @ref hash_iteratively
constexpr std::size_t hash_depth_limit() noexcept
{
return 128;
}
template<typename BasicJsonType> template<typename BasicJsonType>
std::size_t hash_iteratively(const BasicJsonType& j); std::size_t hash_iteratively(const BasicJsonType& j);
@@ -47,7 +41,7 @@ null, 0, 0U, and false, etc.
Hashing an array or an object hashes its elements, which used to call this Hashing an array or an object hashes its elements, which used to call this
function again once per nesting level, so a value nested deeply enough function again once per nesting level, so a value nested deeply enough
exhausted the call stack and terminated the process. The descent is bounded exhausted the call stack and terminated the process. The descent is bounded
here: once @ref hash_depth_limit levels have been entered, @ref here: once @ref recursion_depth_limit levels have been entered, @ref
hash_iteratively hashes what is left without the call stack. A value nested hash_iteratively hashes what is left without the call stack. A value nested
less deeply than that - all but a vanishing minority - is hashed exactly as less deeply than that - all but a vanishing minority - is hashed exactly as
before, without allocating. before, without allocating.
@@ -76,7 +70,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::object: case BasicJsonType::value_t::object:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
return hash_iteratively(j); return hash_iteratively(j);
} }
@@ -93,7 +87,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::array: case BasicJsonType::value_t::array:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
return hash_iteratively(j); return hash_iteratively(j);
} }
@@ -175,7 +169,7 @@ struct hash_frame
Computes the same value as @ref hash, keeping the arrays and objects it has Computes the same value as @ref hash, keeping the arrays and objects it has
entered on an explicit stack instead of descending into them. Only reached for entered on an explicit stack instead of descending into them. Only reached for
values nested deeper than @ref hash_depth_limit. values nested deeper than @ref recursion_depth_limit.
@tparam BasicJsonType basic_json specialization @tparam BasicJsonType basic_json specialization
@param j array or object to hash @param j array or object to hash
@@ -191,7 +185,9 @@ std::size_t hash_iteratively(const BasicJsonType& j)
while (true) while (true)
{ {
hash_frame<BasicJsonType>& frame = stack.back(); // a copy, as entering an element below can reallocate the stack; the
// frame itself is only changed through stack.back()
const hash_frame<BasicJsonType> frame = stack.back();
if (frame.position == frame.value->cend()) if (frame.position == frame.value->cend())
{ {
@@ -209,13 +205,12 @@ std::size_t hash_iteratively(const BasicJsonType& j)
if (frame.value->is_object()) if (frame.value->is_object())
{ {
frame.seed = combine(frame.seed, std::hash<string_t> {}(frame.position.key())); stack.back().seed = combine(stack.back().seed, std::hash<string_t> {}(frame.position.key()));
} }
// read the element and advance before entering it: entering can // advance before entering the element, which pushes onto the stack
// reallocate the stack and so invalidate `frame`
const BasicJsonType& element = *frame.position; const BasicJsonType& element = *frame.position;
++frame.position; ++stack.back().position;
if (element.is_structured()) if (element.is_structured())
{ {
@@ -223,7 +218,7 @@ std::size_t hash_iteratively(const BasicJsonType& j)
} }
else else
{ {
frame.seed = combine(frame.seed, hash(element)); stack.back().seed = combine(stack.back().seed, hash(element));
} }
} }
} }
+5 -11
View File
@@ -30,6 +30,7 @@
#include <nlohmann/detail/meta/cpp_future.hpp> #include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/output/binary_writer.hpp> #include <nlohmann/detail/output/binary_writer.hpp>
#include <nlohmann/detail/output/output_adapters.hpp> #include <nlohmann/detail/output/output_adapters.hpp>
#include <nlohmann/detail/recursion_depth_limit.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
@@ -133,7 +134,7 @@ class serializer
Serializing a container descends into its elements, so a value nested deeply Serializing a container descends into its elements, so a value nested deeply
enough used to exhaust the call stack and terminate the process with no enough used to exhaust the call stack and terminate the process with no
exception to catch. The descent is bounded here: once @ref dump_depth_limit exception to catch. The descent is bounded here: once @ref recursion_depth_limit
levels have been entered, @ref dump_iteratively writes out what is left levels have been entered, @ref dump_iteratively writes out what is left
without the call stack. A value nested less deeply than that - all but a without the call stack. A value nested less deeply than that - all but a
vanishing minority - is written by exactly the code that always wrote it. vanishing minority - is written by exactly the code that always wrote it.
@@ -148,7 +149,7 @@ class serializer
{ {
case value_t::object: case value_t::object:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
dump_iteratively(val, current_indent); dump_iteratively(val, current_indent);
return; return;
@@ -223,7 +224,7 @@ class serializer
case value_t::array: case value_t::array:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
dump_iteratively(val, current_indent); dump_iteratively(val, current_indent);
return; return;
@@ -408,19 +409,12 @@ class serializer
} }
private: private:
/// the number of levels @ref dump_internal descends into before it hands
/// over to @ref dump_iteratively
static constexpr std::size_t dump_depth_limit()
{
return 128;
}
/*! /*!
@brief write out @a val and everything below it without the call stack @brief write out @a val and everything below it without the call stack
Emits the same bytes as @ref dump_internal, keeping the containers it has Emits the same bytes as @ref dump_internal, keeping the containers it has
entered on an explicit stack instead of descending into them. Only reached entered on an explicit stack instead of descending into them. Only reached
for values nested deeper than @ref dump_depth_limit, which is why it is not for values nested deeper than @ref recursion_depth_limit, which is why it is not
written for speed: walking every value this way measured up to 20% slower on written for speed: walking every value this way measured up to 20% slower on
object-heavy documents than letting the compiler drive the descent. object-heavy documents than letting the compiler drive the descent.
*/ */
@@ -0,0 +1,35 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#pragma once
#include <cstddef> // size_t
#include <nlohmann/detail/abi_macros.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (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.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
{
return 128;
}
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
+1
View File
@@ -68,6 +68,7 @@
#include <nlohmann/detail/output/binary_writer.hpp> #include <nlohmann/detail/output/binary_writer.hpp>
#include <nlohmann/detail/output/output_adapters.hpp> #include <nlohmann/detail/output/output_adapters.hpp>
#include <nlohmann/detail/output/serializer.hpp> #include <nlohmann/detail/output/serializer.hpp>
#include <nlohmann/detail/recursion_depth_limit.hpp>
#include <nlohmann/detail/value_t.hpp> #include <nlohmann/detail/value_t.hpp>
#include <nlohmann/json_fwd.hpp> #include <nlohmann/json_fwd.hpp>
#include <nlohmann/ordered_map.hpp> #include <nlohmann/ordered_map.hpp>
+57 -28
View File
@@ -6988,6 +6988,44 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/abi_macros.hpp> // #include <nlohmann/detail/abi_macros.hpp>
// #include <nlohmann/detail/recursion_depth_limit.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <cstddef> // size_t
// #include <nlohmann/detail/abi_macros.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
/*!
@brief the number of nesting levels an operation recurses into
Operations that walk a value (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.
@sa https://github.com/nlohmann/json/issues/5387
*/
constexpr std::size_t recursion_depth_limit() noexcept
{
return 128;
}
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/value_t.hpp> // #include <nlohmann/detail/value_t.hpp>
@@ -7002,13 +7040,6 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
return seed; return seed;
} }
/// the number of levels @ref hash descends into before handing over to
/// @ref hash_iteratively
constexpr std::size_t hash_depth_limit() noexcept
{
return 128;
}
template<typename BasicJsonType> template<typename BasicJsonType>
std::size_t hash_iteratively(const BasicJsonType& j); std::size_t hash_iteratively(const BasicJsonType& j);
@@ -7022,7 +7053,7 @@ null, 0, 0U, and false, etc.
Hashing an array or an object hashes its elements, which used to call this Hashing an array or an object hashes its elements, which used to call this
function again once per nesting level, so a value nested deeply enough function again once per nesting level, so a value nested deeply enough
exhausted the call stack and terminated the process. The descent is bounded exhausted the call stack and terminated the process. The descent is bounded
here: once @ref hash_depth_limit levels have been entered, @ref here: once @ref recursion_depth_limit levels have been entered, @ref
hash_iteratively hashes what is left without the call stack. A value nested hash_iteratively hashes what is left without the call stack. A value nested
less deeply than that - all but a vanishing minority - is hashed exactly as less deeply than that - all but a vanishing minority - is hashed exactly as
before, without allocating. before, without allocating.
@@ -7051,7 +7082,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::object: case BasicJsonType::value_t::object:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
return hash_iteratively(j); return hash_iteratively(j);
} }
@@ -7068,7 +7099,7 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::array: case BasicJsonType::value_t::array:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= hash_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
return hash_iteratively(j); return hash_iteratively(j);
} }
@@ -7150,7 +7181,7 @@ struct hash_frame
Computes the same value as @ref hash, keeping the arrays and objects it has Computes the same value as @ref hash, keeping the arrays and objects it has
entered on an explicit stack instead of descending into them. Only reached for entered on an explicit stack instead of descending into them. Only reached for
values nested deeper than @ref hash_depth_limit. values nested deeper than @ref recursion_depth_limit.
@tparam BasicJsonType basic_json specialization @tparam BasicJsonType basic_json specialization
@param j array or object to hash @param j array or object to hash
@@ -7166,7 +7197,9 @@ std::size_t hash_iteratively(const BasicJsonType& j)
while (true) while (true)
{ {
hash_frame<BasicJsonType>& frame = stack.back(); // a copy, as entering an element below can reallocate the stack; the
// frame itself is only changed through stack.back()
const hash_frame<BasicJsonType> frame = stack.back();
if (frame.position == frame.value->cend()) if (frame.position == frame.value->cend())
{ {
@@ -7184,13 +7217,12 @@ std::size_t hash_iteratively(const BasicJsonType& j)
if (frame.value->is_object()) if (frame.value->is_object())
{ {
frame.seed = combine(frame.seed, std::hash<string_t> {}(frame.position.key())); stack.back().seed = combine(stack.back().seed, std::hash<string_t> {}(frame.position.key()));
} }
// read the element and advance before entering it: entering can // advance before entering the element, which pushes onto the stack
// reallocate the stack and so invalidate `frame`
const BasicJsonType& element = *frame.position; const BasicJsonType& element = *frame.position;
++frame.position; ++stack.back().position;
if (element.is_structured()) if (element.is_structured())
{ {
@@ -7198,7 +7230,7 @@ std::size_t hash_iteratively(const BasicJsonType& j)
} }
else else
{ {
frame.seed = combine(frame.seed, hash(element)); stack.back().seed = combine(stack.back().seed, hash(element));
} }
} }
} }
@@ -22297,6 +22329,8 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/output/output_adapters.hpp> // #include <nlohmann/detail/output/output_adapters.hpp>
// #include <nlohmann/detail/recursion_depth_limit.hpp>
// #include <nlohmann/detail/string_concat.hpp> // #include <nlohmann/detail/string_concat.hpp>
// #include <nlohmann/detail/value_t.hpp> // #include <nlohmann/detail/value_t.hpp>
@@ -22402,7 +22436,7 @@ class serializer
Serializing a container descends into its elements, so a value nested deeply Serializing a container descends into its elements, so a value nested deeply
enough used to exhaust the call stack and terminate the process with no enough used to exhaust the call stack and terminate the process with no
exception to catch. The descent is bounded here: once @ref dump_depth_limit exception to catch. The descent is bounded here: once @ref recursion_depth_limit
levels have been entered, @ref dump_iteratively writes out what is left levels have been entered, @ref dump_iteratively writes out what is left
without the call stack. A value nested less deeply than that - all but a without the call stack. A value nested less deeply than that - all but a
vanishing minority - is written by exactly the code that always wrote it. vanishing minority - is written by exactly the code that always wrote it.
@@ -22417,7 +22451,7 @@ class serializer
{ {
case value_t::object: case value_t::object:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
dump_iteratively(val, current_indent); dump_iteratively(val, current_indent);
return; return;
@@ -22492,7 +22526,7 @@ class serializer
case value_t::array: case value_t::array:
{ {
if (JSON_HEDLEY_UNLIKELY(depth >= dump_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()))
{ {
dump_iteratively(val, current_indent); dump_iteratively(val, current_indent);
return; return;
@@ -22677,19 +22711,12 @@ class serializer
} }
private: private:
/// the number of levels @ref dump_internal descends into before it hands
/// over to @ref dump_iteratively
static constexpr std::size_t dump_depth_limit()
{
return 128;
}
/*! /*!
@brief write out @a val and everything below it without the call stack @brief write out @a val and everything below it without the call stack
Emits the same bytes as @ref dump_internal, keeping the containers it has Emits the same bytes as @ref dump_internal, keeping the containers it has
entered on an explicit stack instead of descending into them. Only reached entered on an explicit stack instead of descending into them. Only reached
for values nested deeper than @ref dump_depth_limit, which is why it is not for values nested deeper than @ref recursion_depth_limit, which is why it is not
written for speed: walking every value this way measured up to 20% slower on written for speed: walking every value this way measured up to 20% slower on
object-heavy documents than letting the compiler drive the descent. object-heavy documents than letting the compiler drive the descent.
*/ */
@@ -24002,6 +24029,8 @@ class serializer
} // namespace detail } // namespace detail
NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/recursion_depth_limit.hpp>
// #include <nlohmann/detail/value_t.hpp> // #include <nlohmann/detail/value_t.hpp>
// #include <nlohmann/json_fwd.hpp> // #include <nlohmann/json_fwd.hpp>
+2 -2
View File
@@ -20,7 +20,7 @@ namespace
// how detail::hash defines the hash of an array or object: the seeds of the // how detail::hash defines the hash of an array or object: the seeds of the
// elements, combined in order. Recursive, so only usable on values nested a // elements, combined in order. Recursive, so only usable on values nested a
// few hundred levels deep - which is exactly what is needed to check that the // few hundred levels deep - which is exactly what is needed to check that the
// iterative path taken below detail::hash_depth_limit() computes the same. // iterative path taken below detail::recursion_depth_limit() computes the same.
template<typename BasicJsonType> template<typename BasicJsonType>
std::size_t reference_hash(const BasicJsonType& j) std::size_t reference_hash(const BasicJsonType& j)
{ {
@@ -189,7 +189,7 @@ TEST_CASE("hash of deeply nested values")
SECTION("hashing past the descent bound computes the same values") SECTION("hashing past the descent bound computes the same values")
{ {
// every depth on either side of where the iterative path takes over // every depth on either side of where the iterative path takes over
for (std::size_t depth = 0; depth <= 2 * nlohmann::detail::hash_depth_limit() + 10; ++depth) for (std::size_t depth = 0; depth <= 2 * nlohmann::detail::recursion_depth_limit() + 10; ++depth)
{ {
CAPTURE(depth); CAPTURE(depth);
const auto arrays = nested<json>(depth, false); const auto arrays = nested<json>(depth, false);