mirror of
https://github.com/nlohmann/json.git
synced 2026-10-15 18:57:13 +00:00
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:
1 parent
ca7740fef2
commit
4530d9681c
4 files changed
+439
-118
No files matched your search
@@ -9,9 +9,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <limits> // numeric_limits
|
||||
#include <map> // map, multimap
|
||||
#include <string> // char_traits
|
||||
#include <tuple> // tuple
|
||||
#include <type_traits> // false_type, is_constructible, is_integral, is_same, true_type
|
||||
#include <unordered_map> // unordered_map, unordered_multimap
|
||||
#include <utility> // declval
|
||||
#include <vector> // vector
|
||||
#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>
|
||||
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)
|
||||
template < typename T, typename U, enable_if_t < !std::is_same<T, U>::value, int > = 0 >
|
||||
T conditional_static_cast(U value)
|
||||
|
||||
+74
-59
@@ -1079,11 +1079,16 @@ public:
|
||||
return next;
|
||||
}
|
||||
|
||||
/// @brief the capacity of the stored array, or unknown_size()
|
||||
/// Only JSON_DIAGNOSTICS uses the value, to detect a reallocation that
|
||||
/// would invalidate the parent pointers. Array types that do not have a
|
||||
/// capacity() member function report unknown_size(), which is treated as
|
||||
/// "the elements may have moved".
|
||||
/// @brief the capacity of the stored array or object, or unknown_size()
|
||||
/// Only JSON_DIAGNOSTICS uses the value, to detect an insertion that may
|
||||
/// have moved the other elements and so invalidated their parent pointers.
|
||||
/// unknown_size() means "the elements may have moved". Arrays only grow at
|
||||
/// 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
|
||||
template < typename A = array_t, detail::enable_if_t < detail::has_capacity<A>::value, int > = 0 >
|
||||
std::size_t array_capacity() const noexcept
|
||||
@@ -1096,57 +1101,63 @@ public:
|
||||
{
|
||||
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
|
||||
static constexpr std::size_t array_capacity() noexcept
|
||||
static constexpr std::size_t container_capacity() noexcept
|
||||
{
|
||||
return detail::unknown_size();
|
||||
}
|
||||
#endif
|
||||
|
||||
/// @brief set the parent of a value that has just been added to an array
|
||||
/// @param j the added value
|
||||
/// @param old_capacity the value @ref array_capacity() returned before the
|
||||
/// @brief set the parent of a value that has just been added to an array or object
|
||||
/// @param j the added (or looked-up) value
|
||||
/// @param old_capacity the value @ref container_capacity() returned before the
|
||||
/// 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
|
||||
// see https://github.com/nlohmann/json/issues/2838
|
||||
JSON_ASSERT(type() == value_t::array);
|
||||
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 elements may have moved, so update all parents
|
||||
// the capacity has changed, or the container type does not let us
|
||||
// tell: the elements may have moved, so update all parents
|
||||
set_parents();
|
||||
return j;
|
||||
}
|
||||
#else
|
||||
static_cast<void>(old_capacity);
|
||||
#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
|
||||
// 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;
|
||||
#else
|
||||
static_cast<void>(j);
|
||||
#endif
|
||||
return j;
|
||||
}
|
||||
@@ -2136,9 +2147,11 @@ public:
|
||||
|
||||
|
||||
/// @brief restore the parent pointers after erasing from an object
|
||||
/// ordered_json keeps its members in a vector, and erasing a member
|
||||
/// re-constructs every member after it in place, which resets their
|
||||
/// parent pointers
|
||||
/// Unless the object type is known to keep its elements in place (see
|
||||
/// detail::has_stable_elements), erasing a member may move the members
|
||||
/// 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()
|
||||
{
|
||||
#if JSON_DIAGNOSTICS
|
||||
@@ -2146,7 +2159,7 @@ public:
|
||||
#pragma warning(push )
|
||||
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
|
||||
#endif
|
||||
if (detail::is_ordered_map<object_t>::value)
|
||||
if (!detail::has_stable_elements<object_t>::value)
|
||||
{
|
||||
set_parents();
|
||||
}
|
||||
@@ -2557,7 +2570,6 @@ public:
|
||||
copy_leaf_value(other, *this);
|
||||
}
|
||||
|
||||
set_parents();
|
||||
assert_invariant();
|
||||
}
|
||||
|
||||
@@ -3445,7 +3457,7 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/at/
|
||||
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
|
||||
@@ -3459,7 +3471,7 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/at/
|
||||
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
|
||||
@@ -3468,7 +3480,7 @@ public:
|
||||
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
|
||||
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
|
||||
@@ -3513,13 +3525,13 @@ public:
|
||||
#if JSON_DIAGNOSTICS
|
||||
// remember array size & capacity before resizing
|
||||
const auto old_size = m_data.m_value.array->size();
|
||||
const auto old_capacity = array_capacity();
|
||||
const auto old_capacity = container_capacity();
|
||||
#endif
|
||||
m_data.m_value.array->resize(idx + 1);
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|
||||
|| array_capacity() != old_capacity))
|
||||
|| container_capacity() != old_capacity))
|
||||
{
|
||||
// capacity has changed: update all parents
|
||||
set_parents();
|
||||
@@ -3566,8 +3578,9 @@ public:
|
||||
// operator[] only works for objects
|
||||
if (JSON_HEDLEY_LIKELY(is_object()))
|
||||
{
|
||||
const auto old_capacity = container_capacity();
|
||||
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");
|
||||
@@ -3617,8 +3630,9 @@ public:
|
||||
// operator[] only works for objects
|
||||
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);
|
||||
return set_parent(result.first->second);
|
||||
return set_parent_after_insert(result.first->second, old_capacity);
|
||||
}
|
||||
|
||||
throw_subscript_wrong_type("string");
|
||||
@@ -4524,9 +4538,9 @@ public:
|
||||
}
|
||||
|
||||
// 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));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4555,9 +4569,9 @@ public:
|
||||
}
|
||||
|
||||
// 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);
|
||||
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
|
||||
@@ -4585,8 +4599,9 @@ public:
|
||||
}
|
||||
|
||||
// add the element to the object
|
||||
const auto old_capacity = container_capacity();
|
||||
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
|
||||
@@ -4639,9 +4654,9 @@ public:
|
||||
}
|
||||
|
||||
// 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)...);
|
||||
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
|
||||
@@ -4662,8 +4677,9 @@ public:
|
||||
}
|
||||
|
||||
// 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)...);
|
||||
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
|
||||
auto it = begin();
|
||||
@@ -4962,9 +4978,9 @@ public:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// set_parent() also repairs the other members, which ordered_json
|
||||
// relocates when adding a key makes its vector grow
|
||||
set_parent(m_data.m_value.object->operator[](it.key()) = std::move(it.value()));
|
||||
// operator[] repairs the other members if the object type moves
|
||||
// them; assigning through it keeps this O(1) per key
|
||||
operator[](it.key()) = std::move(it.value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5014,9 +5030,8 @@ public:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// set_parent() also repairs the other members, which ordered_json
|
||||
// relocates when adding a key makes its vector grow
|
||||
target->set_parent(target->m_data.m_value.object->operator[](first.key()) = std::move(first.value()));
|
||||
// operator[] repairs the other members if the object type moves them
|
||||
target->operator[](first.key()) = std::move(first.value());
|
||||
++first;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user