Fix quadratic parent updates for ordered_json with JSON_DIAGNOSTICS (#5832)

* Fix quadratic parent updates for ordered_json with JSON_DIAGNOSTICS

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Only trust containers known to keep their elements in place

Address review: decide whether an insertion may have moved the other
members by naming the containers that are known not to, instead of the
ones that are known to.

- detail::has_stable_elements: std::map, std::multimap,
  std::unordered_map and std::unordered_multimap; object_capacity()
  reports 0 for them.
- detail::is_nlohmann_ordered_map: only the real nlohmann::ordered_map
  (which only appends) reports its capacity().
- Any other object type reports unknown_size(), so it gets a full
  set_parents() on every insertion. Before, an object type without
  capacity() was treated as never moving, and one with capacity() (a
  sorted flat map, say) was trusted to move only on reallocation.
- set_parents_after_object_erase() re-parents unless the object type is
  stable; the unused set_parent() is removed.
- Comments say "the object type" instead of "ordered_json".
- New test with an object type that moves its members without changing
  its capacity; it fails without this change.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-15 20:01:21 +02:00
1 parent ca7740fef2
commit 4530d9681c
4 files changed
+439 -118

No files matched your search

@@ -9,9 +9,11 @@
#pragma once #pragma once
#include <limits> // numeric_limits #include <limits> // numeric_limits
#include <map> // map, multimap
#include <string> // char_traits #include <string> // char_traits
#include <tuple> // tuple #include <tuple> // tuple
#include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type #include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type
#include <unordered_map> // unordered_map, unordered_multimap
#include <utility> // declval #include <utility> // declval
#include <vector> // vector #include <vector> // vector
#if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L #if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L
@@ -897,6 +899,31 @@ struct has_capacity : std::integral_constant<bool, is_detected<detect_capacity,
template <typename T> template <typename T>
struct is_ordered_map : has_capacity<T> {}; struct is_ordered_map : has_capacity<T> {};
// whether T is a specialization of nlohmann::ordered_map, which only ever
// appends members, so they can only move when its capacity changes
template<typename T>
struct is_nlohmann_ordered_map : std::false_type {};
template<class Key, class T, class IgnoredLess, class Allocator>
struct is_nlohmann_ordered_map<ordered_map<Key, T, IgnoredLess, Allocator>> : std::true_type {};
// whether adding or erasing elements never moves the other elements of the
// container T; only node-based standard containers are known to guarantee this
template<typename T>
struct has_stable_elements : std::false_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::map<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::multimap<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_map<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_multimap<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
// to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324) // to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324)
template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 > template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 >
T conditional_static_cast(U value) T conditional_static_cast(U value)
+74 -59
View File
@@ -1079,11 +1079,16 @@ public:
return next; return next;
} }
/// @brief the capacity of the stored array, or unknown_size() /// @brief the capacity of the stored array or object, or unknown_size()
/// Only JSON_DIAGNOSTICS uses the value, to detect a reallocation that /// Only JSON_DIAGNOSTICS uses the value, to detect an insertion that may
/// would invalidate the parent pointers. Array types that do not have a /// have moved the other elements and so invalidated their parent pointers.
/// capacity() member function report unknown_size(), which is treated as /// unknown_size() means "the elements may have moved". Arrays only grow at
/// "the elements may have moved". /// the end, so their capacity tells whether they reallocated; arrays
/// without a capacity() member report unknown_size(). For objects, only
/// types known to keep their elements in place (std::map and the other
/// node-based standard containers) report a fixed 0, and only
/// nlohmann::ordered_map, which appends members, reports its capacity; any
/// other object type (a sorted flat map, say) reports unknown_size().
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
template < typename A = array_t, detail::enable_if_t < detail::has_capacity<A>::value, int > = 0 > template < typename A = array_t, detail::enable_if_t < detail::has_capacity<A>::value, int > = 0 >
std::size_t array_capacity() const noexcept std::size_t array_capacity() const noexcept
@@ -1096,57 +1101,63 @@ public:
{ {
return detail::unknown_size(); return detail::unknown_size();
} }
template < typename O = object_t, detail::enable_if_t < detail::has_stable_elements<O>::value, int > = 0 >
static constexpr std::size_t object_capacity() noexcept
{
return 0;
}
template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements<O>::value && detail::is_nlohmann_ordered_map<O>::value, int > = 0 >
std::size_t object_capacity() const noexcept
{
return m_data.m_value.object->capacity();
}
template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements<O>::value && !detail::is_nlohmann_ordered_map<O>::value, int > = 0 >
static constexpr std::size_t object_capacity() noexcept
{
return detail::unknown_size();
}
std::size_t container_capacity() const noexcept
{
return is_array() ? array_capacity() : object_capacity();
}
#else #else
static constexpr std::size_t array_capacity() noexcept static constexpr std::size_t container_capacity() noexcept
{ {
return detail::unknown_size(); return detail::unknown_size();
} }
#endif #endif
/// @brief set the parent of a value that has just been added to an array /// @brief set the parent of a value that has just been added to an array or object
/// @param j the added value /// @param j the added (or looked-up) value
/// @param old_capacity the value @ref array_capacity() returned before the /// @param old_capacity the value @ref container_capacity() returned before the
/// insertion /// insertion
reference set_parent_after_array_insert(reference j, std::size_t old_capacity) reference set_parent_after_insert(reference j, std::size_t old_capacity)
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// see https://github.com/nlohmann/json/issues/2838 // see https://github.com/nlohmann/json/issues/2838
JSON_ASSERT(type() == value_t::array);
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|| array_capacity() != old_capacity)) || container_capacity() != old_capacity))
{ {
// the capacity has changed, or the array type does not let us tell: // the capacity has changed, or the container type does not let us
// the elements may have moved, so update all parents // tell: the elements may have moved, so update all parents
set_parents(); set_parents();
return j; return j;
} }
#else #else
static_cast<void>(old_capacity); static_cast<void>(old_capacity);
#endif #endif
return set_parent(j); return set_parent_at(j);
} }
reference set_parent(reference j) /// @brief set the parent of an element accessed via at(); never moves elements
reference set_parent_at(reference j) noexcept
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// ordered_json uses a vector internally, so pointers could have
// been invalidated; see https://github.com/nlohmann/json/issues/2962
#ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif
if (detail::is_ordered_map<object_t>::value)
{
set_parents();
return j;
}
#ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning( pop )
#endif
j.m_parent = this; j.m_parent = this;
#else
static_cast<void>(j);
#endif #endif
return j; return j;
} }
@@ -2136,9 +2147,11 @@ public:
/// @brief restore the parent pointers after erasing from an object /// @brief restore the parent pointers after erasing from an object
/// ordered_json keeps its members in a vector, and erasing a member /// Unless the object type is known to keep its elements in place (see
/// re-constructs every member after it in place, which resets their /// detail::has_stable_elements), erasing a member may move the members
/// parent pointers /// after it; ordered_json, for instance, keeps its members in a vector
/// and re-constructs every member after the erased one in place, which
/// resets their parent pointers
void set_parents_after_object_erase() void set_parents_after_object_erase()
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
@@ -2146,7 +2159,7 @@ public:
#pragma warning(push ) #pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif #endif
if (detail::is_ordered_map<object_t>::value) if (!detail::has_stable_elements<object_t>::value)
{ {
set_parents(); set_parents();
} }
@@ -2557,7 +2570,6 @@ public:
copy_leaf_value(other, *this); copy_leaf_value(other, *this);
} }
set_parents();
assert_invariant(); assert_invariant();
} }
@@ -3445,7 +3457,7 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/at/ /// @sa https://json.nlohmann.me/api/basic_json/at/
reference at(size_type idx) reference at(size_type idx)
{ {
return set_parent(array_at(*this, idx)); return set_parent_at(array_at(*this, idx));
} }
/// @brief access specified array element with bounds checking /// @brief access specified array element with bounds checking
@@ -3459,7 +3471,7 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/at/ /// @sa https://json.nlohmann.me/api/basic_json/at/
reference at(const typename object_t::key_type& key) reference at(const typename object_t::key_type& key)
{ {
return set_parent(object_at(*this, key)); return set_parent_at(object_at(*this, key));
} }
/// @brief access specified object element with bounds checking /// @brief access specified object element with bounds checking
@@ -3468,7 +3480,7 @@ public:
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0> detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
reference at(KeyType && key) reference at(KeyType && key)
{ {
return set_parent(object_at(*this, std::forward<KeyType>(key))); return set_parent_at(object_at(*this, std::forward<KeyType>(key)));
} }
/// @brief access specified object element with bounds checking /// @brief access specified object element with bounds checking
@@ -3513,13 +3525,13 @@ public:
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// remember array size & capacity before resizing // remember array size & capacity before resizing
const auto old_size = m_data.m_value.array->size(); const auto old_size = m_data.m_value.array->size();
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
#endif #endif
m_data.m_value.array->resize(idx + 1); m_data.m_value.array->resize(idx + 1);
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|| array_capacity() != old_capacity)) || container_capacity() != old_capacity))
{ {
// capacity has changed: update all parents // capacity has changed: update all parents
set_parents(); set_parents();
@@ -3566,8 +3578,9 @@ public:
// operator[] only works for objects // operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object())) if (JSON_HEDLEY_LIKELY(is_object()))
{ {
const auto old_capacity = container_capacity();
auto result = m_data.m_value.object->emplace(std::move(key), nullptr); auto result = m_data.m_value.object->emplace(std::move(key), nullptr);
return set_parent(result.first->second); return set_parent_after_insert(result.first->second, old_capacity);
} }
throw_subscript_wrong_type("string"); throw_subscript_wrong_type("string");
@@ -3617,8 +3630,9 @@ public:
// operator[] only works for objects // operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object())) if (JSON_HEDLEY_LIKELY(is_object()))
{ {
const auto old_capacity = container_capacity();
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr); auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
return set_parent(result.first->second); return set_parent_after_insert(result.first->second, old_capacity);
} }
throw_subscript_wrong_type("string"); throw_subscript_wrong_type("string");
@@ -4524,9 +4538,9 @@ public:
} }
// add the element to the array (move semantics) // add the element to the array (move semantics)
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->push_back(std::move(val)); m_data.m_value.array->push_back(std::move(val));
set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
// if val is moved from, basic_json move constructor marks it null, so we do not call the destructor // if val is moved from, basic_json move constructor marks it null, so we do not call the destructor
} }
@@ -4555,9 +4569,9 @@ public:
} }
// add the element to the array // add the element to the array
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->push_back(val); m_data.m_value.array->push_back(val);
set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
} }
/// @brief add an object to an array /// @brief add an object to an array
@@ -4585,8 +4599,9 @@ public:
} }
// add the element to the object // add the element to the object
const auto old_capacity = container_capacity();
auto res = m_data.m_value.object->insert(val); auto res = m_data.m_value.object->insert(val);
set_parent(res.first->second); set_parent_after_insert(res.first->second, old_capacity);
} }
/// @brief add an object to an object /// @brief add an object to an object
@@ -4639,9 +4654,9 @@ public:
} }
// add the element to the array (perfect forwarding) // add the element to the array (perfect forwarding)
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->emplace_back(std::forward<Args>(args)...); m_data.m_value.array->emplace_back(std::forward<Args>(args)...);
return set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); return set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
} }
/// @brief add an object to an object if key does not exist /// @brief add an object to an object if key does not exist
@@ -4662,8 +4677,9 @@ public:
} }
// add the element to the array (perfect forwarding) // add the element to the array (perfect forwarding)
const auto old_capacity = container_capacity();
auto res = m_data.m_value.object->emplace(std::forward<Args>(args)...); auto res = m_data.m_value.object->emplace(std::forward<Args>(args)...);
set_parent(res.first->second); set_parent_after_insert(res.first->second, old_capacity);
// create a result iterator and set iterator to the result of emplace // create a result iterator and set iterator to the result of emplace
auto it = begin(); auto it = begin();
@@ -4962,9 +4978,9 @@ public:
continue; continue;
} }
} }
// set_parent() also repairs the other members, which ordered_json // operator[] repairs the other members if the object type moves
// relocates when adding a key makes its vector grow // them; assigning through it keeps this O(1) per key
set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); operator[](it.key()) = std::move(it.value());
} }
} }
@@ -5014,9 +5030,8 @@ public:
continue; continue;
} }
} }
// set_parent() also repairs the other members, which ordered_json // operator[] repairs the other members if the object type moves them
// relocates when adding a key makes its vector grow target->operator[](first.key()) = std::move(first.value());
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value()));
++first; ++first;
} }
} }
+101 -59
View File
@@ -3743,9 +3743,11 @@ NLOHMANN_JSON_NAMESPACE_END
#include <limits> // numeric_limits #include <limits> // numeric_limits
#include <map> // map, multimap
#include <string> // char_traits #include <string> // char_traits
#include <tuple> // tuple #include <tuple> // tuple
#include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type #include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type
#include <unordered_map> // unordered_map, unordered_multimap
#include <utility> // declval #include <utility> // declval
#include <vector> // vector #include <vector> // vector
#if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L #if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L
@@ -4909,6 +4911,31 @@ struct has_capacity : std::integral_constant<bool, is_detected<detect_capacity,
template <typename T> template <typename T>
struct is_ordered_map : has_capacity<T> {}; struct is_ordered_map : has_capacity<T> {};
// whether T is a specialization of nlohmann::ordered_map, which only ever
// appends members, so they can only move when its capacity changes
template<typename T>
struct is_nlohmann_ordered_map : std::false_type {};
template<class Key, class T, class IgnoredLess, class Allocator>
struct is_nlohmann_ordered_map<ordered_map<Key, T, IgnoredLess, Allocator>> : std::true_type {};
// whether adding or erasing elements never moves the other elements of the
// container T; only node-based standard containers are known to guarantee this
template<typename T>
struct has_stable_elements : std::false_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::map<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Compare, class Allocator>
struct has_stable_elements<std::multimap<Key, T, Compare, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_map<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
template<class Key, class T, class Hash, class KeyEqual, class Allocator>
struct has_stable_elements<std::unordered_multimap<Key, T, Hash, KeyEqual, Allocator>> : std::true_type {};
// to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324) // to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324)
template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 > template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 >
T conditional_static_cast(U value) T conditional_static_cast(U value)
@@ -30833,11 +30860,16 @@ public:
return next; return next;
} }
/// @brief the capacity of the stored array, or unknown_size() /// @brief the capacity of the stored array or object, or unknown_size()
/// Only JSON_DIAGNOSTICS uses the value, to detect a reallocation that /// Only JSON_DIAGNOSTICS uses the value, to detect an insertion that may
/// would invalidate the parent pointers. Array types that do not have a /// have moved the other elements and so invalidated their parent pointers.
/// capacity() member function report unknown_size(), which is treated as /// unknown_size() means "the elements may have moved". Arrays only grow at
/// "the elements may have moved". /// the end, so their capacity tells whether they reallocated; arrays
/// without a capacity() member report unknown_size(). For objects, only
/// types known to keep their elements in place (std::map and the other
/// node-based standard containers) report a fixed 0, and only
/// nlohmann::ordered_map, which appends members, reports its capacity; any
/// other object type (a sorted flat map, say) reports unknown_size().
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
template < typename A = array_t, detail::enable_if_t < detail::has_capacity<A>::value, int > = 0 > template < typename A = array_t, detail::enable_if_t < detail::has_capacity<A>::value, int > = 0 >
std::size_t array_capacity() const noexcept std::size_t array_capacity() const noexcept
@@ -30850,57 +30882,63 @@ public:
{ {
return detail::unknown_size(); return detail::unknown_size();
} }
template < typename O = object_t, detail::enable_if_t < detail::has_stable_elements<O>::value, int > = 0 >
static constexpr std::size_t object_capacity() noexcept
{
return 0;
}
template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements<O>::value && detail::is_nlohmann_ordered_map<O>::value, int > = 0 >
std::size_t object_capacity() const noexcept
{
return m_data.m_value.object->capacity();
}
template < typename O = object_t, detail::enable_if_t < !detail::has_stable_elements<O>::value && !detail::is_nlohmann_ordered_map<O>::value, int > = 0 >
static constexpr std::size_t object_capacity() noexcept
{
return detail::unknown_size();
}
std::size_t container_capacity() const noexcept
{
return is_array() ? array_capacity() : object_capacity();
}
#else #else
static constexpr std::size_t array_capacity() noexcept static constexpr std::size_t container_capacity() noexcept
{ {
return detail::unknown_size(); return detail::unknown_size();
} }
#endif #endif
/// @brief set the parent of a value that has just been added to an array /// @brief set the parent of a value that has just been added to an array or object
/// @param j the added value /// @param j the added (or looked-up) value
/// @param old_capacity the value @ref array_capacity() returned before the /// @param old_capacity the value @ref container_capacity() returned before the
/// insertion /// insertion
reference set_parent_after_array_insert(reference j, std::size_t old_capacity) reference set_parent_after_insert(reference j, std::size_t old_capacity)
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// see https://github.com/nlohmann/json/issues/2838 // see https://github.com/nlohmann/json/issues/2838
JSON_ASSERT(type() == value_t::array);
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|| array_capacity() != old_capacity)) || container_capacity() != old_capacity))
{ {
// the capacity has changed, or the array type does not let us tell: // the capacity has changed, or the container type does not let us
// the elements may have moved, so update all parents // tell: the elements may have moved, so update all parents
set_parents(); set_parents();
return j; return j;
} }
#else #else
static_cast<void>(old_capacity); static_cast<void>(old_capacity);
#endif #endif
return set_parent(j); return set_parent_at(j);
} }
reference set_parent(reference j) /// @brief set the parent of an element accessed via at(); never moves elements
reference set_parent_at(reference j) noexcept
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// ordered_json uses a vector internally, so pointers could have
// been invalidated; see https://github.com/nlohmann/json/issues/2962
#ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif
if (detail::is_ordered_map<object_t>::value)
{
set_parents();
return j;
}
#ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning( pop )
#endif
j.m_parent = this; j.m_parent = this;
#else
static_cast<void>(j);
#endif #endif
return j; return j;
} }
@@ -31890,9 +31928,11 @@ public:
/// @brief restore the parent pointers after erasing from an object /// @brief restore the parent pointers after erasing from an object
/// ordered_json keeps its members in a vector, and erasing a member /// Unless the object type is known to keep its elements in place (see
/// re-constructs every member after it in place, which resets their /// detail::has_stable_elements), erasing a member may move the members
/// parent pointers /// after it; ordered_json, for instance, keeps its members in a vector
/// and re-constructs every member after the erased one in place, which
/// resets their parent pointers
void set_parents_after_object_erase() void set_parents_after_object_erase()
{ {
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
@@ -31900,7 +31940,7 @@ public:
#pragma warning(push ) #pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif #endif
if (detail::is_ordered_map<object_t>::value) if (!detail::has_stable_elements<object_t>::value)
{ {
set_parents(); set_parents();
} }
@@ -32311,7 +32351,6 @@ public:
copy_leaf_value(other, *this); copy_leaf_value(other, *this);
} }
set_parents();
assert_invariant(); assert_invariant();
} }
@@ -33199,7 +33238,7 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/at/ /// @sa https://json.nlohmann.me/api/basic_json/at/
reference at(size_type idx) reference at(size_type idx)
{ {
return set_parent(array_at(*this, idx)); return set_parent_at(array_at(*this, idx));
} }
/// @brief access specified array element with bounds checking /// @brief access specified array element with bounds checking
@@ -33213,7 +33252,7 @@ public:
/// @sa https://json.nlohmann.me/api/basic_json/at/ /// @sa https://json.nlohmann.me/api/basic_json/at/
reference at(const typename object_t::key_type& key) reference at(const typename object_t::key_type& key)
{ {
return set_parent(object_at(*this, key)); return set_parent_at(object_at(*this, key));
} }
/// @brief access specified object element with bounds checking /// @brief access specified object element with bounds checking
@@ -33222,7 +33261,7 @@ public:
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0> detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
reference at(KeyType && key) reference at(KeyType && key)
{ {
return set_parent(object_at(*this, std::forward<KeyType>(key))); return set_parent_at(object_at(*this, std::forward<KeyType>(key)));
} }
/// @brief access specified object element with bounds checking /// @brief access specified object element with bounds checking
@@ -33267,13 +33306,13 @@ public:
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
// remember array size & capacity before resizing // remember array size & capacity before resizing
const auto old_size = m_data.m_value.array->size(); const auto old_size = m_data.m_value.array->size();
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
#endif #endif
m_data.m_value.array->resize(idx + 1); m_data.m_value.array->resize(idx + 1);
#if JSON_DIAGNOSTICS #if JSON_DIAGNOSTICS
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size() if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|| array_capacity() != old_capacity)) || container_capacity() != old_capacity))
{ {
// capacity has changed: update all parents // capacity has changed: update all parents
set_parents(); set_parents();
@@ -33320,8 +33359,9 @@ public:
// operator[] only works for objects // operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object())) if (JSON_HEDLEY_LIKELY(is_object()))
{ {
const auto old_capacity = container_capacity();
auto result = m_data.m_value.object->emplace(std::move(key), nullptr); auto result = m_data.m_value.object->emplace(std::move(key), nullptr);
return set_parent(result.first->second); return set_parent_after_insert(result.first->second, old_capacity);
} }
throw_subscript_wrong_type("string"); throw_subscript_wrong_type("string");
@@ -33371,8 +33411,9 @@ public:
// operator[] only works for objects // operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object())) if (JSON_HEDLEY_LIKELY(is_object()))
{ {
const auto old_capacity = container_capacity();
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr); auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
return set_parent(result.first->second); return set_parent_after_insert(result.first->second, old_capacity);
} }
throw_subscript_wrong_type("string"); throw_subscript_wrong_type("string");
@@ -34278,9 +34319,9 @@ public:
} }
// add the element to the array (move semantics) // add the element to the array (move semantics)
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->push_back(std::move(val)); m_data.m_value.array->push_back(std::move(val));
set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
// if val is moved from, basic_json move constructor marks it null, so we do not call the destructor // if val is moved from, basic_json move constructor marks it null, so we do not call the destructor
} }
@@ -34309,9 +34350,9 @@ public:
} }
// add the element to the array // add the element to the array
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->push_back(val); m_data.m_value.array->push_back(val);
set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
} }
/// @brief add an object to an array /// @brief add an object to an array
@@ -34339,8 +34380,9 @@ public:
} }
// add the element to the object // add the element to the object
const auto old_capacity = container_capacity();
auto res = m_data.m_value.object->insert(val); auto res = m_data.m_value.object->insert(val);
set_parent(res.first->second); set_parent_after_insert(res.first->second, old_capacity);
} }
/// @brief add an object to an object /// @brief add an object to an object
@@ -34393,9 +34435,9 @@ public:
} }
// add the element to the array (perfect forwarding) // add the element to the array (perfect forwarding)
const auto old_capacity = array_capacity(); const auto old_capacity = container_capacity();
m_data.m_value.array->emplace_back(std::forward<Args>(args)...); m_data.m_value.array->emplace_back(std::forward<Args>(args)...);
return set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity); return set_parent_after_insert(m_data.m_value.array->back(), old_capacity);
} }
/// @brief add an object to an object if key does not exist /// @brief add an object to an object if key does not exist
@@ -34416,8 +34458,9 @@ public:
} }
// add the element to the array (perfect forwarding) // add the element to the array (perfect forwarding)
const auto old_capacity = container_capacity();
auto res = m_data.m_value.object->emplace(std::forward<Args>(args)...); auto res = m_data.m_value.object->emplace(std::forward<Args>(args)...);
set_parent(res.first->second); set_parent_after_insert(res.first->second, old_capacity);
// create a result iterator and set iterator to the result of emplace // create a result iterator and set iterator to the result of emplace
auto it = begin(); auto it = begin();
@@ -34716,9 +34759,9 @@ public:
continue; continue;
} }
} }
// set_parent() also repairs the other members, which ordered_json // operator[] repairs the other members if the object type moves
// relocates when adding a key makes its vector grow // them; assigning through it keeps this O(1) per key
set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value())); operator[](it.key()) = std::move(it.value());
} }
} }
@@ -34768,9 +34811,8 @@ public:
continue; continue;
} }
} }
// set_parent() also repairs the other members, which ordered_json // operator[] repairs the other members if the object type moves them
// relocates when adding a key makes its vector grow target->operator[](first.key()) = std::move(first.value());
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value()));
++first; ++first;
} }
} }
+237
View File
@@ -13,9 +13,11 @@
#endif #endif
#define JSON_DIAGNOSTICS 1 #define JSON_DIAGNOSTICS 1
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
using nlohmann::json; using nlohmann::json;
using nlohmann::ordered_json;
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
@@ -631,3 +633,238 @@ TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch()
CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error);
} }
} }
namespace
{
// check that every child's parent pointer points to the containing value
template<typename BasicJsonType>
bool parents_consistent(const BasicJsonType& j)
{
bool ok = true;
if (j.is_array() || j.is_object())
{
for (const auto& child : j)
{
ok = ok && child.m_parent == &j && parents_consistent(child);
}
}
return ok;
}
} // namespace
TEST_CASE_TEMPLATE("Parent pointers stay consistent", J, json, ordered_json)
{
SECTION("parse, copy, move, assign, swap")
{
J j = J::parse(R"({"a":[1,{"b":[2,3]},[4]],"c":{"d":{"e":[]}}})");
CHECK(parents_consistent(j));
J copy(j);
CHECK(parents_consistent(copy));
J moved(std::move(copy));
CHECK(parents_consistent(moved));
J assigned;
assigned = j;
CHECK(parents_consistent(assigned));
J other = {1, 2, {{"k", J::array({1, 2})}}};
other.swap(assigned);
CHECK(parents_consistent(other));
CHECK(parents_consistent(assigned));
}
SECTION("push_back growth")
{
J arr = J::array();
for (int i = 0; i < 500; ++i)
{
arr.push_back(J::array({i}));
}
CHECK(parents_consistent(arr));
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
obj.push_back({std::to_string(i), J::array({i})});
}
CHECK(parents_consistent(obj));
}
SECTION("emplace_back and emplace growth")
{
J arr = J::array();
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
arr.emplace_back(J::array({i}));
obj.emplace(std::to_string(i), J::array({i}));
}
CHECK(parents_consistent(arr));
CHECK(parents_consistent(obj));
}
SECTION("operator[] growth and at()")
{
J obj = J::object();
for (int i = 0; i < 500; ++i)
{
obj[std::to_string(i)] = J::array({i});
CHECK(obj[std::to_string(i / 2)].m_parent == &obj);
}
CHECK(parents_consistent(obj));
J arr = J::array();
for (int i = 0; i < 500; ++i)
{
arr[static_cast<size_t>(i)] = J::object({{"x", i}});
}
for (size_t i = 0; i < 500; ++i)
{
CHECK(arr.at(i).m_parent == &arr);
}
CHECK(obj.at("3").m_parent == &obj);
CHECK(parents_consistent(arr));
CHECK(parents_consistent(obj));
}
}
TEST_CASE_TEMPLATE("update() and merge_patch() keep parent pointers and diagnostic paths", J, json, ordered_json)
{
// enough keys to make the vector behind ordered_json reallocate several times
const auto make_object = [](int first, int last)
{
J o = J::object();
for (int i = first; i < last; ++i)
{
o[std::to_string(i)] = J::object({{"x", J::array({i})}});
}
return o;
};
// const access, so operator[] cannot repair the parent pointers
const auto check_path = [](const J & j, const std::string & key)
{
const std::string expected = "[json.exception.type_error.304] (/" + key + "/x) cannot use at() with array";
CHECK_THROWS_WITH_AS(j[key]["x"].at("y"), expected.c_str(), json::type_error);
};
SECTION("update(const_reference)")
{
J j = make_object(0, 200);
j.update(make_object(100, 400));
CHECK(j.size() == 400);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "150");
check_path(j, "399");
}
SECTION("update(const_reference, merge_objects)")
{
J j = make_object(0, 200);
j["50"]["y"] = 1;
J other = make_object(100, 400);
other["50"] = J::object({{"z", J::array({1})}});
j.update(other, true);
CHECK(j.size() == 400);
CHECK(j["50"].size() == 3);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "399");
}
SECTION("update(first, last)")
{
J j = make_object(0, 200);
J other = make_object(100, 400);
j.update(other.begin(), other.end());
CHECK(j.size() == 400);
CHECK(parents_consistent(j));
check_path(j, "0");
check_path(j, "399");
J k = make_object(0, 200);
k.update(other.begin(), other.end(), true);
CHECK(k.size() == 400);
CHECK(parents_consistent(k));
check_path(k, "399");
}
SECTION("merge_patch()")
{
J j = make_object(0, 200);
J patch = make_object(100, 400);
patch["0"] = nullptr;
j.merge_patch(patch);
CHECK(j.size() == 399);
CHECK_FALSE(j.contains("0"));
CHECK(parents_consistent(j));
check_path(j, "1");
check_path(j, "399");
}
}
namespace
{
// an object type that, like a sorted flat map, moves its members when a key is
// added even if its capacity does not change; it has a capacity() member, but
// is neither node-based nor nlohmann::ordered_map, so the library must not
// trust its capacity to tell whether the members moved
template<class Key, class T, class IgnoredLess = std::less<Key>,
class Allocator = std::allocator<std::pair<const Key, T>>>
struct relocating_map : nlohmann::ordered_map<Key, T, IgnoredLess, Allocator>
{
using base_type = nlohmann::ordered_map<Key, T, IgnoredLess, Allocator>;
using typename base_type::iterator;
using typename base_type::Container;
using base_type::base_type;
template<typename... Args>
std::pair<iterator, bool> emplace(Args&& ... args)
{
auto result = base_type::emplace(std::forward<Args>(args)...);
const auto index = result.first - this->begin();
if (result.second)
{
// move every member to a new buffer of the same capacity
Container relocated;
relocated.reserve(this->capacity());
for (auto& member : *this)
{
relocated.emplace_back(member.first, std::move(member.second));
}
this->Container::swap(relocated);
}
return {this->begin() + index, result.second};
}
};
using relocating_json = nlohmann::basic_json<relocating_map>;
} // namespace
TEST_CASE("Parent pointers stay consistent for object types not known to keep their members in place")
{
relocating_json obj = relocating_json::object();
for (int i = 0; i < 100; ++i)
{
obj[std::to_string(i)] = relocating_json::array({i});
obj.emplace(std::to_string(i + 1000), relocating_json::array({i}));
}
CHECK(parents_consistent(obj));
obj.erase("0");
CHECK(parents_consistent(obj));
relocating_json other = relocating_json::object();
for (int i = 0; i < 100; ++i)
{
other[std::to_string(i + 2000)] = relocating_json::array({i});
}
obj.update(other);
CHECK(parents_consistent(obj));
obj["1"].push_back(42);
CHECK_THROWS_WITH_AS(obj["1"].at(5), "[json.exception.out_of_range.401] (/1) array index 5 is out of range", relocating_json::out_of_range&);
}