From 37006318e3f5452f699bce68107735e715a561fe Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 25 Sep 2026 22:06:07 +0200 Subject: [PATCH] Compare unordered objects by key below the nesting bound Values nested deeper than the nesting bound are compared without the call stack, walking both objects entry by entry. Two equal objects of a type that enumerates its entries in no fixed order - std::unordered_map, say - can be walked in different orders, so they compared unequal, and a deep copy compared unequal to its original. std::unordered_map's own operator== does not depend on the order, which is what applies above the bound. Where the keys differ, equality now finds the entry by its key instead. An ordering, and ordered_map, whose operator== compares its entries in sequence, still decide by the key. Signed-off-by: Niels Lohmann --- include/nlohmann/json.hpp | 25 ++++++++-- single_include/nlohmann/json.hpp | 25 ++++++++-- tests/src/unit-comparison.cpp | 82 ++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index c578fd11a..9faaa057f 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -1492,13 +1492,28 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec 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); + + if (key_result != compare_result::equal) + { + // An object type without a fixed order of its entries - + // std::unordered_map, say - may enumerate two equal + // objects differently, and its operator== does not care. + // Equality then finds the entry by its key; an ordering, + // or an object type that compares its entries in + // sequence (ordered_map), is decided by the key itself. + const auto* rhs_object = current.rhs_value->m_data.m_value.object; + const auto found = (!Ordered && !detail::is_ordered_map::value) + ? rhs_object->find(current.lhs_object_it->first) + : rhs_object->cend(); + if (found == rhs_object->cend()) + { + return key_result; + } + right = &(found->second); + } + ++current.lhs_object_it; ++current.rhs_object_it; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4981c3b02..4ad9df1b7 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -26450,13 +26450,28 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec 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); + + if (key_result != compare_result::equal) + { + // An object type without a fixed order of its entries - + // std::unordered_map, say - may enumerate two equal + // objects differently, and its operator== does not care. + // Equality then finds the entry by its key; an ordering, + // or an object type that compares its entries in + // sequence (ordered_map), is decided by the key itself. + const auto* rhs_object = current.rhs_value->m_data.m_value.object; + const auto found = (!Ordered && !detail::is_ordered_map::value) + ? rhs_object->find(current.lhs_object_it->first) + : rhs_object->cend(); + if (found == rhs_object->cend()) + { + return key_result; + } + right = &(found->second); + } + ++current.lhs_object_it; ++current.rhs_object_it; } diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 68d20aca9..93e646554 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -16,6 +16,9 @@ #include "doctest_compatibility.h" #include +#include +#include +#include #define JSON_TESTS_PRIVATE #include @@ -735,3 +738,82 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24 } } #endif + +namespace +{ +// an object type that enumerates its entries in no fixed order +template +using unordered_object_t = std::unordered_map, std::equal_to, Allocator>; +using unordered_json = nlohmann::basic_json; + +// ten entries; after inserting and erasing many more, the map keeps its +// larger bucket count and enumerates the same entries in another order +unordered_json make_unordered_object(const bool rehashed) +{ + unordered_json j = unordered_json::object(); + const int count = rehashed ? 1000 : 10; + for (int i = 0; i < count; ++i) + { + j[std::to_string(i)] = i; + } + for (int i = 10; i < count; ++i) + { + j.erase(std::to_string(i)); + } + return j; +} + +template +Json nest(Json j, const std::size_t depth) +{ + for (std::size_t i = 0; i < depth; ++i) + { + Json outer = Json::object(); + outer["x"] = std::move(j); + j = std::move(outer); + } + return j; +} +} // namespace + +TEST_CASE("equality of objects whose entries have no fixed order") +{ + // Values nested deeper than a bound are compared without the call stack, + // entry by entry. That must agree with the object type's own operator==, + // which for std::unordered_map does not depend on the order of the + // entries, and for ordered_map does. + for (const std::size_t depth : std::vector {0, 200}) + { + CAPTURE(depth); + + const unordered_json rehashed = nest(make_unordered_object(true), depth); + const unordered_json fresh = nest(make_unordered_object(false), depth); + CHECK(rehashed == fresh); + CHECK_FALSE(rehashed != fresh); + + // a copy is equal to its original + const unordered_json copy = rehashed; // NOLINT(performance-unnecessary-copy-initialization) + CHECK(copy == rehashed); + + // a different value, a different key, or another entry still count + unordered_json other_value = make_unordered_object(true); + other_value["5"] = 42; + CHECK_FALSE(nest(other_value, depth) == fresh); + + unordered_json other_key = make_unordered_object(true); + other_key.erase("5"); + other_key["50"] = 5; + CHECK_FALSE(nest(other_key, depth) == fresh); + + unordered_json more_entries = make_unordered_object(true); + more_entries["10"] = 10; + CHECK_FALSE(nest(more_entries, depth) == fresh); + CHECK_FALSE(fresh == nest(more_entries, depth)); + + // ordered_json compares its entries in sequence + const nlohmann::ordered_json ab = nest(nlohmann::ordered_json({{"a", 1}, {"b", 2}}), depth); + const nlohmann::ordered_json ba = nest(nlohmann::ordered_json({{"b", 2}, {"a", 1}}), depth); + CHECK_FALSE(ab == ba); + CHECK(ab != ba); + } +}