Compare commits

...
Author SHA1 Message Date
Niels Lohmann 01f2d5381b Test unordered object equality without std::unordered_map
basic_json<std::unordered_map> instantiates std::pair<const string,
basic_json> while basic_json is still incomplete. The standard does not
require std::unordered_map to support that, and libstdc++ 6 to 9 as well
as the EDG front ends of icpc and nvc++ reject it, which broke the build
of unit-comparison on those CI jobs.

The test now uses an object type derived from std::map (which, as the
default object type, works everywhere) whose comparator orders keys
ascending or descending as chosen at construction, and whose operator==
does not depend on the order of the entries - the property of
std::unordered_map the test is about.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-26 07:51:57 +02:00
Niels Lohmann 37006318e3 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 <mail@nlohmann.me>
2026-09-25 22:06:07 +02:00
3 changed files with 172 additions and 10 deletions
+20 -5
View File
@@ -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<bool, Ordered> {});
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<object_t>::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;
}
+20 -5
View File
@@ -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<bool, Ordered> {});
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<object_t>::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;
}
+132
View File
@@ -16,6 +16,9 @@
#include "doctest_compatibility.h"
#include <cstdint>
#include <map>
#include <string>
#include <vector>
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp>
@@ -735,3 +738,132 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24
}
}
#endif
namespace
{
// orders keys ascending or descending, as chosen when a map is created
template<class Key>
class directed_less
{
public:
directed_less() = default;
explicit directed_less(const bool descending) noexcept
: m_descending(descending)
{}
bool operator()(const Key& lhs, const Key& rhs) const
{
return m_descending ? rhs < lhs : lhs < rhs;
}
private:
bool m_descending = false;
};
// An object type that, like std::unordered_map, enumerates its entries in no
// fixed order - ascending or descending by key, depending on how the map was
// created - and whose operator== does not depend on that order.
// std::unordered_map itself cannot be used here: the standard does not
// require it to accept an incomplete mapped type such as basic_json, and
// libstdc++ 6 to 9 as well as the EDG front ends of icpc and nvc++ reject
// basic_json<std::unordered_map>. std::map, the default object type, works
// with all supported compilers.
template<class Key, class Value, class /*Compare*/, class Allocator>
struct unordered_object_t : std::map<Key, Value, directed_less<Key>, Allocator>
{
using base_type = std::map<Key, Value, directed_less<Key>, Allocator>;
using base_type::base_type;
friend bool operator==(const unordered_object_t& lhs, const unordered_object_t& rhs)
{
if (lhs.size() != rhs.size())
{
return false;
}
for (const auto& entry : lhs)
{
const auto it = rhs.find(entry.first);
if (it == rhs.end() || !(it->second == entry.second))
{
return false;
}
}
return true;
}
friend bool operator!=(const unordered_object_t& lhs, const unordered_object_t& rhs)
{
return !(lhs == rhs);
}
};
using unordered_json = nlohmann::basic_json<unordered_object_t>;
// the entries "0" to "9", enumerated in ascending or in descending order
unordered_json make_unordered_object(const bool descending)
{
unordered_json j = unordered_json::object_t(directed_less<std::string>(descending));
for (int i = 0; i < 10; ++i)
{
j[std::to_string(i)] = i;
}
return j;
}
template<typename Json>
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 unordered_object_t (as for std::unordered_map) does not
// depend on the order of the entries, and for ordered_map does.
REQUIRE(make_unordered_object(true).begin().key() == "9");
REQUIRE(make_unordered_object(false).begin().key() == "0");
for (const std::size_t depth : std::vector<std::size_t> {0, 200})
{
CAPTURE(depth);
const unordered_json descending = nest(make_unordered_object(true), depth);
const unordered_json ascending = nest(make_unordered_object(false), depth);
CHECK(descending == ascending);
CHECK_FALSE(descending != ascending);
// a copy is equal to its original
const unordered_json copy = descending; // NOLINT(performance-unnecessary-copy-initialization)
CHECK(copy == descending);
// 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) == ascending);
unordered_json other_key = make_unordered_object(true);
other_key.erase("5");
other_key["50"] = 5;
CHECK_FALSE(nest(other_key, depth) == ascending);
unordered_json more_entries = make_unordered_object(true);
more_entries["10"] = 10;
CHECK_FALSE(nest(more_entries, depth) == ascending);
CHECK_FALSE(ascending == 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);
}
}