diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index ec879cf0a..94c1e8d1d 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -846,6 +846,13 @@ 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 /*! @@ -1151,6 +1158,286 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } + /// the result of comparing two values, including values that cannot be + /// 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 + static bool compare_descent_exhausted() 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 + return true; +#else + return 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* + { + switch (result) + { + case compare_result::less: + return std::partial_ordering::less; + case compare_result::greater: + return std::partial_ordering::greater; + case compare_result::equal: + return std::partial_ordering::equivalent; + case compare_result::unordered: + default: + return std::partial_ordering::unordered; + } + } +#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. + */ + template + static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept + { + if (lhs == rhs) + { + return compare_result::equal; + } + + return order_leaves(lhs, rhs, std::integral_constant {}); + } + + /*! + @brief compare two object keys + + An object compares its entries as pairs of a key and a value, so its keys + are compared exactly as std::pair compares them: with < where the objects + are being ordered, and with == where they are only checked for equality. + Note that this is not the object's own comparator, which for a vector-backed + object type such as nlohmann::ordered_map tells equality rather than order. + */ + static compare_result compare_keys(const typename object_t::key_type& lhs, + const typename object_t::key_type& rhs, + std::true_type /*ordered*/) + { + if (lhs < rhs) + { + return compare_result::less; + } + + if (rhs < lhs) + { + return compare_result::greater; + } + + return compare_result::equal; + } + + /// @brief check two object keys for equality + static compare_result compare_keys(const typename object_t::key_type& lhs, + const typename object_t::key_type& rhs, + std::false_type /*ordered*/) + { + return lhs == rhs ? compare_result::equal : compare_result::unordered; + } + + /// @brief tell apart two values that are not equal + /// @note only instantiated where the values are being ordered, as a key or + /// string type is not required to be ordered to be compared for equality + static compare_result order_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + if (lhs < rhs) + { + return compare_result::less; + } + + if (rhs < lhs) + { + return compare_result::greater; + } + + return compare_result::unordered; + } + + /// @brief report two values as not equal without ordering them + static compare_result order_leaves(const_reference /*lhs*/, const_reference /*rhs*/, std::false_type /*ordered*/) noexcept + { + return compare_result::unordered; + } + + /*! + @brief compare @a lhs and @a rhs without descending into them + + Reached once a comparison has descended @ref compare_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 + enumerate them - which is how the container types this library ships compare + themselves: a std::map enumerates its entries in key order, and + 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. + */ + template + static compare_result compare_iteratively(const_reference lhs, const_reference rhs, + const bool unordered_compares_equal) noexcept + { + /// a pair of containers being compared in lockstep + struct frame + { + const basic_json* lhs_value{nullptr}; + const basic_json* rhs_value{nullptr}; + typename array_t::const_iterator lhs_array_it{}; + typename array_t::const_iterator rhs_array_it{}; + typename object_t::const_iterator lhs_object_it{}; + typename object_t::const_iterator rhs_object_it{}; + }; + + std::vector stack; + const basic_json* left = &lhs; + const basic_json* right = &rhs; + + for (;;) + { + const auto type = left->m_data.m_type; + + if (type == right->m_data.m_type && (type == value_t::array || type == value_t::object)) + { + // descend: the elements decide, and are compared further down + stack.emplace_back(); + frame& pushed = stack.back(); + pushed.lhs_value = left; + pushed.rhs_value = right; + + if (type == value_t::array) + { + pushed.lhs_array_it = left->m_data.m_value.array->cbegin(); + pushed.rhs_array_it = right->m_data.m_value.array->cbegin(); + } + else + { + pushed.lhs_object_it = left->m_data.m_value.object->cbegin(); + pushed.rhs_object_it = right->m_data.m_value.object->cbegin(); + } + } + else + { + const compare_result result = compare_leaves(*left, *right); + + // Values that cannot be ordered - a NaN, say - end an ordered + // comparison for std::lexicographical_compare_three_way, but + // std::lexicographical_compare treats them as equivalent and + // carries on with the next element. Both are reproduced here, + // so that a value nested too deeply to descend into compares + // exactly as one that is not. + if (result != compare_result::equal && + !(unordered_compares_equal && result == compare_result::unordered)) + { + return result; + } + } + + // walk back up past the containers that are exhausted, then take the + // next pair of elements from the innermost one that is not + for (;;) + { + if (stack.empty()) + { + return compare_result::equal; + } + + frame& current = stack.back(); + const bool is_object = current.lhs_value->m_data.m_type == value_t::object; + + const bool lhs_done = is_object + ? current.lhs_object_it == current.lhs_value->m_data.m_value.object->cend() + : current.lhs_array_it == current.lhs_value->m_data.m_value.array->cend(); + const bool rhs_done = is_object + ? current.rhs_object_it == current.rhs_value->m_data.m_value.object->cend() + : current.rhs_array_it == current.rhs_value->m_data.m_value.array->cend(); + + if (lhs_done || rhs_done) + { + // whichever ran out first holds the smaller container; if + // both did, they are equal and the container above decides + if (lhs_done != rhs_done) + { + return lhs_done ? compare_result::less : compare_result::greater; + } + + stack.pop_back(); + continue; + } + + if (is_object) + { + // an entry is a key and a value, and the key decides first + const compare_result key_result = + compare_keys(current.lhs_object_it->first, current.rhs_object_it->first, + std::integral_constant {}); + + if (key_result != compare_result::equal) + { + return key_result; + } + + left = &(current.lhs_object_it->second); + right = &(current.rhs_object_it->second); + ++current.lhs_object_it; + ++current.rhs_object_it; + } + else + { + left = &(*current.lhs_array_it); + right = &(*current.rhs_array_it); + ++current.lhs_array_it; + ++current.rhs_array_it; + } + + break; + } + } + } + + public: ////////////////////////// // JSON parser callback // @@ -3943,7 +4230,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // because any negative signed value is smaller than any unsigned value. // Otherwise, the non-negative signed value is cast to unsigned before the // comparison to avoid wraparound. -#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \ +#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \ const auto lhs_type = lhs.type(); \ const auto rhs_type = rhs.type(); \ \ @@ -3952,11 +4239,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (lhs_type) \ { \ case value_t::array: \ + { \ + if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \ + { \ + return (deep_result); \ + } \ + const compare_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(!(may_descend) || compare_descent_exhausted())) \ + { \ + return (deep_result); \ + } \ + const compare_depth_guard guard; \ return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ - \ + } \ + \ case value_t::null: \ return (null_result); \ \ @@ -4056,7 +4357,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma GCC diagnostic ignored "-Wfloat-equal" #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false) + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -4081,7 +4383,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* std::partial_ordering::equivalent, std::partial_ordering::unordered, - lhs_type <=> rhs_type) // *NOPAD* + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) } /// @brief comparison: 3-way @@ -4148,7 +4451,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false) + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -4204,7 +4508,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // default_result is used if we cannot compare values. In that case, // we compare types. Note we have to call the operator explicitly, // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type)) + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) } /// @brief comparison: less than diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 80dea9790..69f9bbd44 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -22200,6 +22200,13 @@ 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 /*! @@ -22505,6 +22512,286 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } + /// the result of comparing two values, including values that cannot be + /// 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 + static bool compare_descent_exhausted() 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 + return true; +#else + return 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* + { + switch (result) + { + case compare_result::less: + return std::partial_ordering::less; + case compare_result::greater: + return std::partial_ordering::greater; + case compare_result::equal: + return std::partial_ordering::equivalent; + case compare_result::unordered: + default: + return std::partial_ordering::unordered; + } + } +#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. + */ + template + static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept + { + if (lhs == rhs) + { + return compare_result::equal; + } + + return order_leaves(lhs, rhs, std::integral_constant {}); + } + + /*! + @brief compare two object keys + + An object compares its entries as pairs of a key and a value, so its keys + are compared exactly as std::pair compares them: with < where the objects + are being ordered, and with == where they are only checked for equality. + Note that this is not the object's own comparator, which for a vector-backed + object type such as nlohmann::ordered_map tells equality rather than order. + */ + static compare_result compare_keys(const typename object_t::key_type& lhs, + const typename object_t::key_type& rhs, + std::true_type /*ordered*/) + { + if (lhs < rhs) + { + return compare_result::less; + } + + if (rhs < lhs) + { + return compare_result::greater; + } + + return compare_result::equal; + } + + /// @brief check two object keys for equality + static compare_result compare_keys(const typename object_t::key_type& lhs, + const typename object_t::key_type& rhs, + std::false_type /*ordered*/) + { + return lhs == rhs ? compare_result::equal : compare_result::unordered; + } + + /// @brief tell apart two values that are not equal + /// @note only instantiated where the values are being ordered, as a key or + /// string type is not required to be ordered to be compared for equality + static compare_result order_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept + { + if (lhs < rhs) + { + return compare_result::less; + } + + if (rhs < lhs) + { + return compare_result::greater; + } + + return compare_result::unordered; + } + + /// @brief report two values as not equal without ordering them + static compare_result order_leaves(const_reference /*lhs*/, const_reference /*rhs*/, std::false_type /*ordered*/) noexcept + { + return compare_result::unordered; + } + + /*! + @brief compare @a lhs and @a rhs without descending into them + + Reached once a comparison has descended @ref compare_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 + enumerate them - which is how the container types this library ships compare + themselves: a std::map enumerates its entries in key order, and + 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. + */ + template + static compare_result compare_iteratively(const_reference lhs, const_reference rhs, + const bool unordered_compares_equal) noexcept + { + /// a pair of containers being compared in lockstep + struct frame + { + const basic_json* lhs_value{nullptr}; + const basic_json* rhs_value{nullptr}; + typename array_t::const_iterator lhs_array_it{}; + typename array_t::const_iterator rhs_array_it{}; + typename object_t::const_iterator lhs_object_it{}; + typename object_t::const_iterator rhs_object_it{}; + }; + + std::vector stack; + const basic_json* left = &lhs; + const basic_json* right = &rhs; + + for (;;) + { + const auto type = left->m_data.m_type; + + if (type == right->m_data.m_type && (type == value_t::array || type == value_t::object)) + { + // descend: the elements decide, and are compared further down + stack.emplace_back(); + frame& pushed = stack.back(); + pushed.lhs_value = left; + pushed.rhs_value = right; + + if (type == value_t::array) + { + pushed.lhs_array_it = left->m_data.m_value.array->cbegin(); + pushed.rhs_array_it = right->m_data.m_value.array->cbegin(); + } + else + { + pushed.lhs_object_it = left->m_data.m_value.object->cbegin(); + pushed.rhs_object_it = right->m_data.m_value.object->cbegin(); + } + } + else + { + const compare_result result = compare_leaves(*left, *right); + + // Values that cannot be ordered - a NaN, say - end an ordered + // comparison for std::lexicographical_compare_three_way, but + // std::lexicographical_compare treats them as equivalent and + // carries on with the next element. Both are reproduced here, + // so that a value nested too deeply to descend into compares + // exactly as one that is not. + if (result != compare_result::equal && + !(unordered_compares_equal && result == compare_result::unordered)) + { + return result; + } + } + + // walk back up past the containers that are exhausted, then take the + // next pair of elements from the innermost one that is not + for (;;) + { + if (stack.empty()) + { + return compare_result::equal; + } + + frame& current = stack.back(); + const bool is_object = current.lhs_value->m_data.m_type == value_t::object; + + const bool lhs_done = is_object + ? current.lhs_object_it == current.lhs_value->m_data.m_value.object->cend() + : current.lhs_array_it == current.lhs_value->m_data.m_value.array->cend(); + const bool rhs_done = is_object + ? current.rhs_object_it == current.rhs_value->m_data.m_value.object->cend() + : current.rhs_array_it == current.rhs_value->m_data.m_value.array->cend(); + + if (lhs_done || rhs_done) + { + // whichever ran out first holds the smaller container; if + // both did, they are equal and the container above decides + if (lhs_done != rhs_done) + { + return lhs_done ? compare_result::less : compare_result::greater; + } + + stack.pop_back(); + continue; + } + + if (is_object) + { + // an entry is a key and a value, and the key decides first + const compare_result key_result = + compare_keys(current.lhs_object_it->first, current.rhs_object_it->first, + std::integral_constant {}); + + if (key_result != compare_result::equal) + { + return key_result; + } + + left = &(current.lhs_object_it->second); + right = &(current.rhs_object_it->second); + ++current.lhs_object_it; + ++current.rhs_object_it; + } + else + { + left = &(*current.lhs_array_it); + right = &(*current.rhs_array_it); + ++current.lhs_array_it; + ++current.rhs_array_it; + } + + break; + } + } + } + + public: ////////////////////////// // JSON parser callback // @@ -25297,7 +25584,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // because any negative signed value is smaller than any unsigned value. // Otherwise, the non-negative signed value is cast to unsigned before the // comparison to avoid wraparound. -#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \ +#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \ const auto lhs_type = lhs.type(); \ const auto rhs_type = rhs.type(); \ \ @@ -25306,11 +25593,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (lhs_type) \ { \ case value_t::array: \ + { \ + if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \ + { \ + return (deep_result); \ + } \ + const compare_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(!(may_descend) || compare_descent_exhausted())) \ + { \ + return (deep_result); \ + } \ + const compare_depth_guard guard; \ return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ - \ + } \ + \ case value_t::null: \ return (null_result); \ \ @@ -25410,7 +25711,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma GCC diagnostic ignored "-Wfloat-equal" #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false) + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -25435,7 +25737,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* std::partial_ordering::equivalent, std::partial_ordering::unordered, - lhs_type <=> rhs_type) // *NOPAD* + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) } /// @brief comparison: 3-way @@ -25502,7 +25805,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false) + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -25558,7 +25862,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // default_result is used if we cannot compare values. In that case, // we compare types. Note we have to call the operator explicitly, // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type)) + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) } /// @brief comparison: less than diff --git a/tests/src/unit-large_json.cpp b/tests/src/unit-large_json.cpp index f10c8be41..0f71bcd2e 100644 --- a/tests/src/unit-large_json.cpp +++ b/tests/src/unit-large_json.cpp @@ -157,6 +157,54 @@ TEST_CASE("tests on deeply nested JSONs") CHECK(deep_depth == depth); } + SECTION("comparing") + { + // Comparing used to descend once per level, and an ordered + // comparison used to compare every pair of elements twice, once in + // each direction, which took exponentially long in the nesting + // depth. Both are gone: these finish in milliseconds, where the + // second used to take longer than anyone would wait even for a + // value nested only a few dozen levels deep. + const std::string text = std::string(depth, '[') + '0' + std::string(depth, ']'); + const json j = json::parse(text); + const json same = json::parse(text); + const json larger = json::parse(std::string(depth, '[') + '1' + std::string(depth, ']')); + + CHECK(j == same); + CHECK_FALSE(j == larger); + CHECK(j != larger); + + CHECK(j < larger); + CHECK_FALSE(larger < j); + CHECK(larger > j); + CHECK(j <= same); + CHECK(j >= same); + + // a value that ends earlier is the smaller one + const json shorter = json::parse(std::string(depth - 1, '[') + '0' + std::string(depth - 1, ']')); + CHECK_FALSE(j == shorter); + } + + SECTION("comparing objects") + { + std::string text; + text.reserve(6 * depth + 1); + for (std::size_t i = 0; i < depth; ++i) + { + text += "{\"a\":"; + } + text += '1'; + text.append(depth, '}'); + + const json j = json::parse(text); + const json same = json::parse(text); + + CHECK(j == same); + CHECK_FALSE(j != same); + CHECK(j <= same); + CHECK(j >= same); + } + SECTION("the copy is independent of the original") { const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));