Preserve the object comparator's state in a deep copy past the nesting bound

copy_object_level(), used by the copy constructor and copy assignment once a
value is nested deeper than the iterative deep copy's bound (128 levels, or
every copy under JSON_NO_THREAD_LOCAL), built each object's copy with the
object type's plain range constructor. That default-constructs the object's
comparator instead of copying the original's. For an object type whose
comparator carries state, such as a std::map that compares keys
case-sensitively only when constructed that way, the copy then ordered - and
could even deduplicate - its keys differently from the original.

Add detail::is_comparator_constructible_object_type, a detection trait for
object types that provide a key_comp() and a constructor taking a range and a
comparator, the way std::map does. copy_object_level now dispatches on it: an
object type that qualifies gets its copy built with src_object.key_comp()
passed along; other object types, such as nlohmann::ordered_map (which has a
key_compare for its std::map-like interface, but no key_comp()), keep using
the plain range constructor exactly as before.

merge_patch and update() were checked for the same pattern; neither is
affected, since both only ever add members one at a time to an object that
already has its own comparator (or start a brand new default-constructed one),
rather than rebuilding an object_t from a range copied out of an existing,
possibly custom-comparator object.

Fixes #5649.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-30 08:14:47 +02:00
parent 633de8e44b
commit 6be8c2e986
4 changed files with 190 additions and 2 deletions
+54 -1
View File
@@ -4199,6 +4199,37 @@ struct actual_object_comparator
template<typename BasicJsonType>
using actual_object_comparator_t = typename actual_object_comparator<BasicJsonType>::type;
template<typename T>
using detect_key_comp = decltype(std::declval<const T&>().key_comp());
// whether ObjectType can be constructed from a pair of Iterator together with
// a copy of its own comparator, the way std::map can: it needs a nested
// key_compare, a const key_comp() convertible to it, and a matching
// (Iterator, Iterator, const key_compare&) constructor.
//
// used to preserve a stateful comparator when a copy is built from a range
// past the iterative deep copy's nesting bound (see copy_object_level); an
// object type that does not satisfy this, such as nlohmann::ordered_map
// (which has key_compare for its std::map-like interface, but no key_comp()),
// keeps default-constructing its comparator, just as it always has
template<typename ObjectType, typename Iterator, typename = void>
struct is_comparator_constructible_object_type_impl : std::false_type {};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type_impl <
ObjectType, Iterator, enable_if_t<is_detected<detect_key_compare, ObjectType>::value >>
{
using key_compare = typename ObjectType::key_compare;
static constexpr bool value =
is_detected_convertible<key_compare, detect_key_comp, ObjectType>::value &&
std::is_constructible<ObjectType, Iterator, Iterator, const key_compare&>::value;
};
template<typename ObjectType, typename Iterator>
struct is_comparator_constructible_object_type
: is_comparator_constructible_object_type_impl<ObjectType, Iterator> {};
/////////////////
// char_traits //
/////////////////
@@ -27208,6 +27239,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
}
/// @brief create the object type from a range, preserving @a src_object's
/// comparator when the object type supports it
/// Enabled for object types that provide a key_comp() and a matching
/// range-plus-comparator constructor, such as std::map. Other object
/// types, such as nlohmann::ordered_map, fall back to the plain range
/// constructor and default-construct their comparator, just as they
/// always have (@ref detail::is_comparator_constructible_object_type).
template < typename Iterator, detail::enable_if_t <
detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int > = 0 >
static object_t* create_object_with_comparator(const object_t& src_object, Iterator first, Iterator last)
{
return create<object_t>(first, last, src_object.key_comp());
}
template < typename Iterator, detail::enable_if_t <
!detail::is_comparator_constructible_object_type<object_t, Iterator>::value, int > = 0 >
static object_t* create_object_with_comparator(const object_t& /*src_object*/, Iterator first, Iterator last)
{
return create<object_t>(first, last);
}
/// @brief create the copy of the object @a src in @a dst
/// @note structured values are appended to @a worklist instead
static void copy_object_level(const basic_json& src, basic_json& dst,
@@ -27225,7 +27277,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
scratch.emplace_back(element.first, basic_json());
}
dst.m_data.m_value.object = create<object_t>(std::make_move_iterator(scratch.begin()),
dst.m_data.m_value.object = create_object_with_comparator(src_object,
std::make_move_iterator(scratch.begin()),
std::make_move_iterator(scratch.end()));
scratch.clear();