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); + } +}