Relax the ArrayType and ObjectType requirements

Two requirements forced users of otherwise suitable containers to write a
wrapper, and neither was load-bearing.

array_t::capacity() was read in push_back(), emplace_back(), operator+=(), and
operator[](size_type), but set_parent() only looks at the value under
JSON_DIAGNOSTICS; without diagnostics it was computed and discarded. Read it
through array_capacity(), which reports unknown_size() when diagnostics are off
or when the array type has no capacity() at all, and treat an unknown capacity
as "the elements may have moved" so the parent pointers are refreshed
conservatively. std::deque now works as ArrayType, in both builds, and
capacity() is no longer named at all in a default build. Since the capacity is
now only meaningful for array insertions, it moves out of set_parent() into
set_parent_after_array_insert().

basic_json::erase(iterator) assigned the object's erase() return value, which
requires the container to return the following iterator. Abseil's hash maps
return void to avoid computing a successor the caller may not need. Detect that
and compute the successor before erasing; containers that return an iterator,
including the vector-backed ordered_map where a precomputed successor would be
wrong, keep the existing path.

Together these leave an Abseil hash map needing only an alias that restores the
template argument order, and no adapter at all for std::deque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hxZxz8svM54c6ATEvXp5E
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-08-28 17:38:56 +00:00
co-authored by Claude Opus 5
parent 96806af2dc
commit 5d93f35463
8 changed files with 344 additions and 79 deletions
@@ -779,6 +779,13 @@ using has_erase_with_key_type = typename std::conditional <
std::true_type,
std::false_type >::type;
template<typename T>
using detect_capacity = decltype(std::declval<const T&>().capacity());
// type trait to check if a type has a capacity() member function
template<typename T>
struct has_capacity : std::integral_constant<bool, is_detected<detect_capacity, T>::value> {};
// a naive helper to check if a type is an ordered_map (exploits the fact that
// ordered_map inherits capacity() from std::vector)
template <typename T>
+75 -20
View File
@@ -783,21 +783,76 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return it;
}
reference set_parent(reference j, std::size_t old_capacity = detail::unknown_size())
/// @brief erase an element from the object and return the following one
/// Not every map returns an iterator from erase(iterator): some containers
/// (e.g., Abseil's hash maps) return void to avoid computing a successor
/// the caller may not need. Compute it before erasing for those.
template < typename It, detail::enable_if_t <
!std::is_void<decltype(std::declval<object_t&>().erase(std::declval<It>()))>::value, int > = 0 >
typename object_t::iterator erase_from_object(It pos)
{
return m_data.m_value.object->erase(pos);
}
template < typename It, detail::enable_if_t <
std::is_void<decltype(std::declval<object_t&>().erase(std::declval<It>()))>::value, int > = 0 >
typename object_t::iterator erase_from_object(It pos)
{
typename object_t::iterator next = std::next(pos);
m_data.m_value.object->erase(pos);
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".
#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
{
return m_data.m_value.array->capacity();
}
template < typename A = array_t, detail::enable_if_t < !detail::has_capacity<A>::value, int > = 0 >
std::size_t array_capacity() const noexcept
{
return detail::unknown_size();
}
#else
static constexpr std::size_t array_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
/// insertion
reference set_parent_after_array_insert(reference j, std::size_t old_capacity)
{
#if JSON_DIAGNOSTICS
if (old_capacity != detail::unknown_size())
// 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))
{
// see https://github.com/nlohmann/json/issues/2838
JSON_ASSERT(type() == value_t::array);
if (JSON_HEDLEY_UNLIKELY(m_data.m_value.array->capacity() != old_capacity))
{
// capacity has changed: update all parents
set_parents();
return j;
}
// the capacity has changed, or the array 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);
}
reference set_parent(reference j)
{
#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
@@ -816,7 +871,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
j.m_parent = this;
#else
static_cast<void>(j);
static_cast<void>(old_capacity);
#endif
return j;
}
@@ -2147,12 +2201,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
#if JSON_DIAGNOSTICS
// remember array size & capacity before resizing
const auto old_size = m_data.m_value.array->size();
const auto old_capacity = m_data.m_value.array->capacity();
const auto old_capacity = array_capacity();
#endif
m_data.m_value.array->resize(idx + 1);
#if JSON_DIAGNOSTICS
if (JSON_HEDLEY_UNLIKELY(m_data.m_value.array->capacity() != old_capacity))
if (JSON_HEDLEY_UNLIKELY(old_capacity == detail::unknown_size()
|| array_capacity() != old_capacity))
{
// capacity has changed: update all parents
set_parents();
@@ -2543,7 +2598,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
case value_t::object:
{
result.m_it.object_iterator = m_data.m_value.object->erase(pos.m_it.object_iterator);
result.m_it.object_iterator = erase_from_object(pos.m_it.object_iterator);
break;
}
@@ -3173,9 +3228,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
// add the element to the array (move semantics)
const auto old_capacity = m_data.m_value.array->capacity();
const auto old_capacity = array_capacity();
m_data.m_value.array->push_back(std::move(val));
set_parent(m_data.m_value.array->back(), old_capacity);
set_parent_after_array_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
}
@@ -3206,9 +3261,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
// add the element to the array
const auto old_capacity = m_data.m_value.array->capacity();
const auto old_capacity = array_capacity();
m_data.m_value.array->push_back(val);
set_parent(m_data.m_value.array->back(), old_capacity);
set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity);
}
/// @brief add an object to an array
@@ -3294,9 +3349,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
// add the element to the array (perfect forwarding)
const auto old_capacity = m_data.m_value.array->capacity();
const auto old_capacity = array_capacity();
m_data.m_value.array->emplace_back(std::forward<Args>(args)...);
return set_parent(m_data.m_value.array->back(), old_capacity);
return set_parent_after_array_insert(m_data.m_value.array->back(), old_capacity);
}
/// @brief add an object to an object if key does not exist