From 596e33c319d78d425df5a9f3085674c32e89113a Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 21 Aug 2026 08:56:43 +0200 Subject: [PATCH] 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 --- include/nlohmann/json.hpp | 144 +++++++++++++++---------------- single_include/nlohmann/json.hpp | 144 +++++++++++++++---------------- 2 files changed, 140 insertions(+), 148 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index a8032076f..ee5327f09 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -846,35 +846,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(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; @@ -883,7 +909,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 @@ -1162,53 +1198,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(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* @@ -1228,18 +1217,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 static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept @@ -1313,7 +1302,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 @@ -1322,6 +1311,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 static compare_result compare_iteratively(const_reference lhs, const_reference rhs, @@ -4247,21 +4243,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); \ } \ \ diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index f6975bcf4..71a93fd9a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -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(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(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 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 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); \ } \ \