diff --git a/docs/mkdocs/docs/api/basic_json/array_t.md b/docs/mkdocs/docs/api/basic_json/array_t.md index 9871b2c12..b6e41a6ad 100644 --- a/docs/mkdocs/docs/api/basic_json/array_t.md +++ b/docs/mkdocs/docs/api/basic_json/array_t.md @@ -14,9 +14,9 @@ To store objects in C++, a type is defined by the template parameters explained ## Template parameters `ArrayType` -: container type to store arrays. It must be a vector-like container: the library uses `operator[]`, `at()`, - `resize()`, and `capacity()`, and requires random-access iterators. `#!cpp std::deque` and `#!cpp std::list` do - not provide `capacity()` and therefore cannot be used as-is -- see +: container type to store arrays. It must be a vector-like container: the library uses `operator[]`, `at()`, and + `resize()`, and requires random-access iterators. `#!cpp std::vector` and `#!cpp std::deque` qualify; + `#!cpp std::list` does not. See [Template Parameter Requirements](../../features/types/template_parameters.md#arraytype) for the full list of requirements. @@ -70,3 +70,4 @@ Arrays are stored as pointers in a `basic_json` type. That is, for any access to ## Version history - Added in version 1.0.0. +- Made `capacity()` optional, so that array types such as `#!cpp std::deque` can be used, in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/object_t.md b/docs/mkdocs/docs/api/basic_json/object_t.md index d171a7a89..6ce393a1d 100644 --- a/docs/mkdocs/docs/api/basic_json/object_t.md +++ b/docs/mkdocs/docs/api/basic_json/object_t.md @@ -126,3 +126,4 @@ the object is silently converted as an array of key-value pairs, which is incorr ## Version history - Added in version 1.0.0. +- Allowed object types whose `erase(iterator)` returns `#!cpp void` in version 3.13.0. diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index 1f3935244..07d7c2eb8 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -29,7 +29,7 @@ Requirements are split into two groups: | Template parameter | Default | Notable substitutes | |-------------------------------------------------------------------|-----------------------------------|------------------------------------------------------------------------| | [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps | -| [`ArrayType`](#arraytype) | `std::vector` | vector-like containers only | +| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` | | [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` | | [`BooleanType`](#booleantype) | `bool` | none worth using | | [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type | @@ -79,7 +79,8 @@ i.e., the template arguments follow the order and meaning of `std::map`. - Constructors: default, copy, move, and from an iterator range `(first, last)`. - Member functions `begin()`, `end()`, `cbegin()`, `cend()`, `empty()`, `size()`, `max_size()`, `clear()`, `find(key)`, `count(key)`, `emplace(key, value)`, `insert(value_type)`, `insert(first, last)`, `operator[](key)`, - `erase(iterator)`, `erase(first, last)`, and `erase(key)`. + `erase(iterator)`, `erase(first, last)`, and `erase(key)`. `erase(iterator)` may return the following iterator or + `#!cpp void`; in the latter case the library computes the successor itself, before erasing. - `emplace` and `insert(value_type)` must return `#!cpp std::pair` and must have **unique-key** semantics; multimaps cannot be used. - The type must be swappable (via `std::swap` or an ADL `swap`). @@ -123,33 +124,17 @@ similar containers are integrated; see [Object Order](../object_order.md). #### Abseil hash maps `absl::flat_hash_map` and `absl::node_hash_map` tolerate an incomplete value type, but they take a hash function as -their third template argument and their `erase(iterator)` returns `#!cpp void` rather than the following iterator. An -adapter that fixes both makes them usable: +their third template argument. The same adapter as for `#!cpp std::unordered_map` makes them usable: ```cpp -template class Map> -struct absl_object +template +struct flat_hash_object + : absl::flat_hash_map, std::equal_to, Allocator> { - template - struct type : Map, std::equal_to, Allocator> - { - using base_t = Map, std::equal_to, Allocator>; - using base_t::base_t; - using iterator = typename base_t::iterator; - using base_t::erase; - - iterator erase(iterator pos) - { - iterator next = std::next(pos); - base_t::erase(pos); - return next; - } - }; + using base_t = absl::flat_hash_map, std::equal_to, Allocator>; + using base_t::base_t; }; -template -using flat_hash_object = typename absl_object::template type; - using flat_hash_json = nlohmann::basic_json; ``` @@ -204,8 +189,7 @@ using array_t = ArrayType>; - Constructors: default, copy, move, from an iterator range `(first, last)`, and from `(count, value)`. - Member functions `begin()`, `end()`, `cbegin()`, `cend()`, `empty()`, `size()`, `max_size()`, `clear()`, `operator[](size_type)`, `at(size_type)`, `back()`, `push_back()`, `emplace_back()`, `pop_back()`, `resize()`, - `insert()` (single element, count, range, and initializer list), `erase(pos)`, `erase(first, last)`, and - **`capacity()`**. + `insert()` (single element, count, range, and initializer list), `erase(pos)`, and `erase(first, last)`. - `iterator` must be default-constructible, and it as well as the type returned by `cbegin()`/`cend()` must satisfy [LegacyRandomAccessIterator](https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator). A `#!cpp static_assert` only checks for @@ -215,23 +199,21 @@ using array_t = ArrayType>; [`basic_json::iterator`](../../api/basic_json/begin.md) require random access. - The type must be swappable and provide the comparison operators `==`, `!=`, `<`, `<=`, `>`, `>=` (or `<=>`). -!!! note "`capacity()` is required unconditionally" +!!! note "`capacity()` is optional" - [`push_back`](../../api/basic_json/push_back.md), [`emplace_back`](../../api/basic_json/emplace_back.md), - [`operator+=`](../../api/basic_json/operator+=.md), and - [`operator[]`](../../api/basic_json/operator%5B%5D.md) with an array index read `array_t::capacity()` to - detect reallocations, regardless of whether [`JSON_DIAGNOSTICS`](../../api/macros/json_diagnostics.md) is enabled. - Consequently `#!cpp std::deque` and `#!cpp std::list` cannot be used as `ArrayType` as-is. A `std::deque` becomes - usable when wrapped in a type that adds a `capacity()` member function; `#!cpp std::list` additionally lacks - `operator[]` and random-access iterators and cannot be used at all. + With [`JSON_DIAGNOSTICS`](../../api/macros/json_diagnostics.md) enabled, the library reads `array_t::capacity()` + to find out whether adding an element reallocated the array and moved its elements, which would invalidate the + parent pointers. An array type without a `capacity()` member function is handled conservatively: the parent + pointers of all elements are refreshed after every insertion, which makes adding *n* elements cost O(*n*²). Only + diagnostics builds pay this; without them `capacity()` is never called. ### Compatible containers | Container | Support | |-------------------------------|----------------------------------------------------------------------------------| | `#!cpp std::vector` (default) | full | -| `#!cpp std::deque` | only when wrapped in a type that adds a `capacity()` member function | -| `#!cpp std::list` | not usable; no `operator[]`, no `capacity()`, and no random-access iterators | +| `#!cpp std::deque` | full; keeps references valid while the array grows, but see the note on `capacity()` above | +| `#!cpp std::list` | not usable; no `operator[]` and no random-access iterators | | `absl::InlinedVector` | not usable; requires a complete value type | | `absl::FixedArray` | not usable; the size is fixed at construction | diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 36eaaab79..350048b87 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -779,6 +779,13 @@ using has_erase_with_key_type = typename std::conditional < std::true_type, std::false_type >::type; +template +using detect_capacity = decltype(std::declval().capacity()); + +// type trait to check if a type has a capacity() member function +template +struct has_capacity : std::integral_constant::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 diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 4e057068c..d6ca60804 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -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().erase(std::declval()))>::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().erase(std::declval()))>::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::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::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(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(j); - static_cast(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)...); - 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 diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index ebf3153a3..e56c0970f 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4542,6 +4542,13 @@ using has_erase_with_key_type = typename std::conditional < std::true_type, std::false_type >::type; +template +using detect_capacity = decltype(std::declval().capacity()); + +// type trait to check if a type has a capacity() member function +template +struct has_capacity : std::integral_constant::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 @@ -22192,21 +22199,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().erase(std::declval()))>::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().erase(std::declval()))>::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::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::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(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 @@ -22225,7 +22287,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec j.m_parent = this; #else static_cast(j); - static_cast(old_capacity); #endif return j; } @@ -23556,12 +23617,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(); @@ -23952,7 +24014,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; } @@ -24582,9 +24644,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 } @@ -24615,9 +24677,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 @@ -24703,9 +24765,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)...); - 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 diff --git a/tests/src/unit-custom-array-type.cpp b/tests/src/unit-custom-array-type.cpp new file mode 100644 index 000000000..7e6ff0dc4 --- /dev/null +++ b/tests/src/unit-custom-array-type.cpp @@ -0,0 +1,89 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include + +namespace +{ + +// std::deque has no capacity() member function, which the library only needs +// to detect a reallocation for JSON_DIAGNOSTICS +using deque_json = nlohmann::basic_json; + +} // namespace + +TEST_CASE("array type without capacity()") +{ + SECTION("adding elements") + { + deque_json j = deque_json::array(); + j.push_back(1); + j.push_back("two"); + j.emplace_back(3); + j += 4; + + CHECK(j.size() == 4); + CHECK(j == deque_json({1, "two", 3, 4})); + CHECK(j.back() == 4); + CHECK(j.front() == 1); + } + + SECTION("accessing and modifying elements") + { + auto j = deque_json::parse(R"([1,2,3])"); + + CHECK(j[1] == 2); + CHECK(j.at(2) == 3); + + // growing through operator[] fills up with null values + j[5] = 6; + CHECK(j.size() == 6); + CHECK(j[4].is_null()); + CHECK(j[5] == 6); + + j.erase(0); + CHECK(j == deque_json({2, 3, nullptr, nullptr, 6})); + + auto it = j.erase(j.begin()); + CHECK(*it == 3); + + j.insert(j.begin(), 1); + CHECK(j.front() == 1); + } + + SECTION("serialization and deserialization") + { + const auto j = deque_json::parse(R"({"a":[1,[2,3]],"b":[]})"); + CHECK(j.dump() == R"({"a":[1,[2,3]],"b":[]})"); + CHECK(deque_json::parse(j.dump()) == j); + CHECK(deque_json::from_cbor(deque_json::to_cbor(j)) == j); + + // empty containers are flattened to null and cannot be restored + const auto nested = deque_json::parse(R"({"a":[1,[2,3]]})"); + CHECK(nested.flatten().unflatten() == nested); + } + + SECTION("references stay valid while the array grows") + { + deque_json j = deque_json::array(); + j.push_back(1); + auto& first = j[0]; + for (int i = 0; i < 100; ++i) + { + j.push_back(i); + } + CHECK(&first == &j[0]); + CHECK(first == 1); + } +} diff --git a/tests/src/unit-custom-object-type.cpp b/tests/src/unit-custom-object-type.cpp index 56b853d91..3f37ed73c 100644 --- a/tests/src/unit-custom-object-type.cpp +++ b/tests/src/unit-custom-object-type.cpp @@ -35,8 +35,76 @@ struct unordered_map_object using unordered_json = nlohmann::basic_json; +// An ObjectType whose erase(iterator) returns void rather than the following +// iterator, as for instance Abseil's hash maps do +template +struct void_erase_map : std::map +{ + using base_t = std::map; + using base_t::base_t; + using iterator = typename base_t::iterator; + using base_t::erase; + + void erase(iterator pos) + { + base_t::erase(pos); + } +}; + +using void_erase_json = nlohmann::basic_json; + } // namespace +TEST_CASE("object type whose erase() returns void") +{ + SECTION("erasing every element through the returned iterator") + { + void_erase_json j; + for (int i = 0; i < 8; ++i) + { + j["k" + std::to_string(i)] = i; + } + + std::size_t erased = 0; + for (auto it = j.begin(); it != j.end(); ++erased) + { + it = j.erase(it); + } + CHECK(erased == 8); + CHECK(j.empty()); + } + + SECTION("erasing in the middle returns the following element") + { + void_erase_json j; + for (int i = 0; i < 4; ++i) + { + j["k" + std::to_string(i)] = i; + } + + auto it = j.begin(); + ++it; + const auto after = j.erase(it); + CHECK(j.size() == 3); + CHECK(after.key() == "k2"); + CHECK(after.value() == 2); + CHECK(!j.contains("k1")); + } + + SECTION("the other erase overloads are unaffected") + { + void_erase_json j; + j["a"] = 1; + j["b"] = 2; + j["c"] = 3; + + CHECK(j.erase("a") == 1); + CHECK(j.erase("nope") == 0); + j.erase(j.begin(), j.end()); + CHECK(j.empty()); + } +} + TEST_CASE("object type without key_compare") { SECTION("object_comparator_t falls back to default_object_comparator_t")