diff --git a/docs/mkdocs/docs/api/basic_json/insert.md b/docs/mkdocs/docs/api/basic_json/insert.md index 12619a2d8..992010010 100644 --- a/docs/mkdocs/docs/api/basic_json/insert.md +++ b/docs/mkdocs/docs/api/basic_json/insert.md @@ -65,7 +65,9 @@ the same index, which is now a different value. ## Exception safety -Strong exception safety: if an exception occurs, the original value stays intact. +Strong exception safety: if an exception occurs, the original value stays intact. This includes an exception thrown +while the new elements are copied (for example, `std::bad_alloc`): elements and members that were already added +are removed again. ## Exceptions diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 7bed813df..0708e6f19 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4935,9 +4935,83 @@ public: return result; } - /// @brief inserts element into array - /// @sa https://json.nlohmann.me/api/basic_json/insert/ - iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) + private: + /// whether copying a value cannot throw: the types whose copy allocates nothing + /// (a mask of the types, tested with one shift) + static bool copy_cannot_throw(const basic_json& v) noexcept + { + constexpr unsigned scalar_types = (1U << static_cast(value_t::null)) | + (1U << static_cast(value_t::boolean)) | + (1U << static_cast(value_t::number_integer)) | + (1U << static_cast(value_t::number_unsigned)) | + (1U << static_cast(value_t::number_float)) | + (1U << static_cast(value_t::discarded)); + return ((1U << static_cast(v.m_data.m_type)) & scalar_types) != 0; + } + + /// Undoes an array insertion that did not complete: erases the elements + /// appended at the end and sets the parents, which the container may have + /// left stale when it reallocated. Does nothing on the success path but + /// setting the parents. + class array_append_rollback + { + public: + array_append_rollback(basic_json* v, std::size_t old_size) noexcept + : m_value(v), m_old_size(old_size) {} + array_append_rollback(const array_append_rollback&) = delete; + array_append_rollback& operator=(const array_append_rollback&) = delete; + + void commit() noexcept + { + m_committed = true; + } + + ~array_append_rollback() + { + if (!m_committed) + { + auto& arr = *m_value->m_data.m_value.array; + arr.erase(arr.begin() + static_cast(m_old_size), arr.end()); + } + m_value->set_parents(); + } + + private: + basic_json* m_value; + std::size_t m_old_size; + bool m_committed = false; + }; + + /// Inserts the elements that fill() appends to the array at pos. Copying + /// elements into the middle of a vector (vector::insert) leaves the array + /// changed when a copy throws. Appending is safe (elements are moved on + /// reallocation, and moving a basic_json cannot throw): if fill() throws, + /// the appended elements are erased again; if it succeeds, they are + /// rotated into place, which cannot throw either. + template + iterator insert_by_appending(const const_iterator& pos, Fill fill) + { + JSON_ASSERT(m_data.m_value.array != nullptr); + auto& arr = *m_data.m_value.array; + const auto old_size = arr.size(); + const auto insert_pos = std::distance(arr.begin(), pos.m_it.array_iterator); + + array_append_rollback rollback(this, old_size); + fill(arr); + if (static_cast(insert_pos) != old_size) + { + std::rotate(arr.begin() + insert_pos, arr.begin() + static_cast(old_size), arr.end()); + } + rollback.commit(); + + iterator result(this); + result.m_it.array_iterator = arr.begin() + insert_pos; + return result; + } + + /// inserts a copy of val after checking the arguments; copy before the array is touched + JSON_HEDLEY_NEVER_INLINE + iterator insert_checked(const const_iterator& pos, const basic_json& val) { // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) @@ -4951,7 +5025,187 @@ public: throw_iterator_from_other_value(this); } - // insert to array and return iterator + basic_json tmp(val); + return insert_iterator(pos, std::move(tmp)); + } + + /// inserts cnt copies of val after checking the arguments + JSON_HEDLEY_NEVER_INLINE + iterator insert_checked(const const_iterator& pos, size_type cnt, const basic_json& val) + { + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) + { + throw_insert_wrong_type(*this); + } + + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + if (copy_cannot_throw(val)) + { + return insert_iterator(pos, cnt, val); + } + + return insert_by_appending(pos, [cnt, &val](array_t& arr) + { + // no reserve(): val may refer to an element of this array + for (size_type i = 0; i < cnt; ++i) + { + arr.push_back(val); + } + }); + } + + /// inserts copies of the elements of the array range [first, last); appending + /// them is no slower than vector::insert, also for numbers, so no type check + template + JSON_HEDLEY_NEVER_INLINE + iterator insert_range(const_iterator pos, ArrayIterator first, ArrayIterator last) // NOLINT(performance-unnecessary-value-param) + { + return insert_by_appending(pos, [first, last](array_t& arr) + { + detail::reserve_array(arr, arr.size() + static_cast(std::distance(first, last)), detail::priority_tag<1> {}); + for (auto it = first; it != last; ++it) + { + arr.push_back(*it); + } + }); + } + + /// inserts member v at the end of the object, with a hint where the container takes one + template + static auto insert_member(Object& o, const typename Object::value_type& v, detail::priority_tag<1> /*unused*/) + -> decltype(o.insert(o.end(), v), void()) + { + o.insert(o.end(), v); + } + + template + static void insert_member(Object& o, const typename Object::value_type& v, detail::priority_tag<0> /*unused*/) + { + o.insert(v); + } + + /// Undoes an object range insertion into an empty object that did not + /// complete: all members present were added by it, so clear the object. + class object_clear_on_failure + { + public: + explicit object_clear_on_failure(basic_json* v) noexcept : m_value(v) {} + object_clear_on_failure(const object_clear_on_failure&) = delete; + object_clear_on_failure& operator=(const object_clear_on_failure&) = delete; + + void commit() noexcept + { + m_committed = true; + } + + ~object_clear_on_failure() + { + if (!m_committed) + { + m_value->m_data.m_value.object->clear(); + } + } + + private: + basic_json* m_value; + bool m_committed = false; + }; + + /// Undoes an object range insertion that did not complete: erases the + /// members it added, then sets the parents. Which members were added is + /// remembered as one bit per source member (no allocation for the first + /// 256 members), so the happy path costs a size comparison per member. + template + class object_insert_rollback + { +public: + object_insert_rollback(basic_json* v, SourceIterator first) : m_value(v), m_first(first) {} + object_insert_rollback(const object_insert_rollback&) = delete; + object_insert_rollback& operator=(const object_insert_rollback&) = delete; + + /// call before each run of up to 64 members is inserted; may throw, which leaves the log complete + void start_chunk() + { + if (m_count != 0) + { + if (m_flushed < inline_words) + { + m_inline[m_flushed] = m_word; + } + else + { + m_more.push_back(m_word); + } + ++m_flushed; + m_word = 0; + } + } + + /// call after a member was inserted (or found to exist) + void record(bool added) noexcept + { + m_word |= static_cast(added) << (m_count % 64); + ++m_count; + } + + void commit() noexcept + { + m_committed = true; + } + + ~object_insert_rollback() + { + if (!m_committed) + { + auto& object = *m_value->m_data.m_value.object; + auto it = m_first; + for (std::size_t i = 0; i < m_count; ++i, ++it) + { + const std::size_t chunk = i / 64; + const std::uint64_t word = chunk >= m_flushed ? m_word : (chunk < inline_words ? m_inline[chunk] : m_more[chunk - inline_words]); + if (((word >> (i % 64)) & 1U) != 0) + { + object.erase(it->first); + } + } + } + m_value->set_parents(); + } + +private: + static constexpr std::size_t inline_words = 4; + + basic_json* m_value; + SourceIterator m_first; + bool m_committed = false; + std::size_t m_count = 0; + std::size_t m_flushed = 0; + std::uint64_t m_word = 0; + std::uint64_t m_inline[inline_words] {}; + std::vector m_more {}; + }; + + public: + /// @brief inserts element into array + /// @sa https://json.nlohmann.me/api/basic_json/insert/ + iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) + { + // The common cases are inlined: an array, a fitting iterator, and either + // a value whose copy cannot throw or the end position (vector::insert + // has no effects on failure there). Everything else, including the + // exceptions, is out of line. + if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || + (!copy_cannot_throw(val) && pos.m_it.array_iterator != m_data.m_value.array->end()))) + { + return insert_checked(pos, val); + } + return insert_iterator(pos, val); } @@ -4981,19 +5235,12 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/insert/ iterator insert(const_iterator pos, size_type cnt, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { - // insert only works for arrays - if (JSON_HEDLEY_UNLIKELY(!is_array())) + // see the single-element overload + if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || !copy_cannot_throw(val))) { - throw_insert_wrong_type(*this); + return insert_checked(pos, cnt, val); } - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - throw_iterator_from_other_value(this); - } - - // insert to array and return iterator return insert_iterator(pos, cnt, val); } @@ -5030,8 +5277,7 @@ public: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } - // insert to array and return iterator - return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator); + return insert_range(pos, first.m_it.array_iterator, last.m_it.array_iterator); } /// @brief inserts elements from initializer list into array @@ -5084,8 +5330,34 @@ public: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } - m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); - set_parents(); + // Strong exception safety: range insertion into the container offers + // only the basic guarantee, so insert member by member (as the + // container does) and, if one insertion throws, erase the members this + // call added. + auto& object = *m_data.m_value.object; + if (object.empty()) + { + // all members are new, so the container's own (faster) range + // insertion can be used, and a failure leaves only new members + object_clear_on_failure clear_on_failure(this); + object.insert(first.m_it.object_iterator, last.m_it.object_iterator); + clear_on_failure.commit(); + set_parents(); + return; + } + + object_insert_rollback rollback(this, first.m_it.object_iterator); + for (auto it = first.m_it.object_iterator; it != last.m_it.object_iterator;) + { + rollback.start_chunk(); + for (std::size_t i = 0; i < 64 && it != last.m_it.object_iterator; ++i, ++it) + { + const auto old_size = object.size(); + insert_member(object, *it, detail::priority_tag<1> {}); + rollback.record(object.size() != old_size); + } + } + rollback.commit(); } /// @brief updates a JSON object from another object, overwriting existing keys diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 1fc214c5f..d2eb9eaa1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -42727,9 +42727,83 @@ public: return result; } - /// @brief inserts element into array - /// @sa https://json.nlohmann.me/api/basic_json/insert/ - iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) + private: + /// whether copying a value cannot throw: the types whose copy allocates nothing + /// (a mask of the types, tested with one shift) + static bool copy_cannot_throw(const basic_json& v) noexcept + { + constexpr unsigned scalar_types = (1U << static_cast(value_t::null)) | + (1U << static_cast(value_t::boolean)) | + (1U << static_cast(value_t::number_integer)) | + (1U << static_cast(value_t::number_unsigned)) | + (1U << static_cast(value_t::number_float)) | + (1U << static_cast(value_t::discarded)); + return ((1U << static_cast(v.m_data.m_type)) & scalar_types) != 0; + } + + /// Undoes an array insertion that did not complete: erases the elements + /// appended at the end and sets the parents, which the container may have + /// left stale when it reallocated. Does nothing on the success path but + /// setting the parents. + class array_append_rollback + { + public: + array_append_rollback(basic_json* v, std::size_t old_size) noexcept + : m_value(v), m_old_size(old_size) {} + array_append_rollback(const array_append_rollback&) = delete; + array_append_rollback& operator=(const array_append_rollback&) = delete; + + void commit() noexcept + { + m_committed = true; + } + + ~array_append_rollback() + { + if (!m_committed) + { + auto& arr = *m_value->m_data.m_value.array; + arr.erase(arr.begin() + static_cast(m_old_size), arr.end()); + } + m_value->set_parents(); + } + + private: + basic_json* m_value; + std::size_t m_old_size; + bool m_committed = false; + }; + + /// Inserts the elements that fill() appends to the array at pos. Copying + /// elements into the middle of a vector (vector::insert) leaves the array + /// changed when a copy throws. Appending is safe (elements are moved on + /// reallocation, and moving a basic_json cannot throw): if fill() throws, + /// the appended elements are erased again; if it succeeds, they are + /// rotated into place, which cannot throw either. + template + iterator insert_by_appending(const const_iterator& pos, Fill fill) + { + JSON_ASSERT(m_data.m_value.array != nullptr); + auto& arr = *m_data.m_value.array; + const auto old_size = arr.size(); + const auto insert_pos = std::distance(arr.begin(), pos.m_it.array_iterator); + + array_append_rollback rollback(this, old_size); + fill(arr); + if (static_cast(insert_pos) != old_size) + { + std::rotate(arr.begin() + insert_pos, arr.begin() + static_cast(old_size), arr.end()); + } + rollback.commit(); + + iterator result(this); + result.m_it.array_iterator = arr.begin() + insert_pos; + return result; + } + + /// inserts a copy of val after checking the arguments; copy before the array is touched + JSON_HEDLEY_NEVER_INLINE + iterator insert_checked(const const_iterator& pos, const basic_json& val) { // insert only works for arrays if (JSON_HEDLEY_UNLIKELY(!is_array())) @@ -42743,7 +42817,187 @@ public: throw_iterator_from_other_value(this); } - // insert to array and return iterator + basic_json tmp(val); + return insert_iterator(pos, std::move(tmp)); + } + + /// inserts cnt copies of val after checking the arguments + JSON_HEDLEY_NEVER_INLINE + iterator insert_checked(const const_iterator& pos, size_type cnt, const basic_json& val) + { + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) + { + throw_insert_wrong_type(*this); + } + + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + if (copy_cannot_throw(val)) + { + return insert_iterator(pos, cnt, val); + } + + return insert_by_appending(pos, [cnt, &val](array_t& arr) + { + // no reserve(): val may refer to an element of this array + for (size_type i = 0; i < cnt; ++i) + { + arr.push_back(val); + } + }); + } + + /// inserts copies of the elements of the array range [first, last); appending + /// them is no slower than vector::insert, also for numbers, so no type check + template + JSON_HEDLEY_NEVER_INLINE + iterator insert_range(const_iterator pos, ArrayIterator first, ArrayIterator last) // NOLINT(performance-unnecessary-value-param) + { + return insert_by_appending(pos, [first, last](array_t& arr) + { + detail::reserve_array(arr, arr.size() + static_cast(std::distance(first, last)), detail::priority_tag<1> {}); + for (auto it = first; it != last; ++it) + { + arr.push_back(*it); + } + }); + } + + /// inserts member v at the end of the object, with a hint where the container takes one + template + static auto insert_member(Object& o, const typename Object::value_type& v, detail::priority_tag<1> /*unused*/) + -> decltype(o.insert(o.end(), v), void()) + { + o.insert(o.end(), v); + } + + template + static void insert_member(Object& o, const typename Object::value_type& v, detail::priority_tag<0> /*unused*/) + { + o.insert(v); + } + + /// Undoes an object range insertion into an empty object that did not + /// complete: all members present were added by it, so clear the object. + class object_clear_on_failure + { + public: + explicit object_clear_on_failure(basic_json* v) noexcept : m_value(v) {} + object_clear_on_failure(const object_clear_on_failure&) = delete; + object_clear_on_failure& operator=(const object_clear_on_failure&) = delete; + + void commit() noexcept + { + m_committed = true; + } + + ~object_clear_on_failure() + { + if (!m_committed) + { + m_value->m_data.m_value.object->clear(); + } + } + + private: + basic_json* m_value; + bool m_committed = false; + }; + + /// Undoes an object range insertion that did not complete: erases the + /// members it added, then sets the parents. Which members were added is + /// remembered as one bit per source member (no allocation for the first + /// 256 members), so the happy path costs a size comparison per member. + template + class object_insert_rollback + { +public: + object_insert_rollback(basic_json* v, SourceIterator first) : m_value(v), m_first(first) {} + object_insert_rollback(const object_insert_rollback&) = delete; + object_insert_rollback& operator=(const object_insert_rollback&) = delete; + + /// call before each run of up to 64 members is inserted; may throw, which leaves the log complete + void start_chunk() + { + if (m_count != 0) + { + if (m_flushed < inline_words) + { + m_inline[m_flushed] = m_word; + } + else + { + m_more.push_back(m_word); + } + ++m_flushed; + m_word = 0; + } + } + + /// call after a member was inserted (or found to exist) + void record(bool added) noexcept + { + m_word |= static_cast(added) << (m_count % 64); + ++m_count; + } + + void commit() noexcept + { + m_committed = true; + } + + ~object_insert_rollback() + { + if (!m_committed) + { + auto& object = *m_value->m_data.m_value.object; + auto it = m_first; + for (std::size_t i = 0; i < m_count; ++i, ++it) + { + const std::size_t chunk = i / 64; + const std::uint64_t word = chunk >= m_flushed ? m_word : (chunk < inline_words ? m_inline[chunk] : m_more[chunk - inline_words]); + if (((word >> (i % 64)) & 1U) != 0) + { + object.erase(it->first); + } + } + } + m_value->set_parents(); + } + +private: + static constexpr std::size_t inline_words = 4; + + basic_json* m_value; + SourceIterator m_first; + bool m_committed = false; + std::size_t m_count = 0; + std::size_t m_flushed = 0; + std::uint64_t m_word = 0; + std::uint64_t m_inline[inline_words] {}; + std::vector m_more {}; + }; + + public: + /// @brief inserts element into array + /// @sa https://json.nlohmann.me/api/basic_json/insert/ + iterator insert(const_iterator pos, const basic_json& val) // NOLINT(performance-unnecessary-value-param) + { + // The common cases are inlined: an array, a fitting iterator, and either + // a value whose copy cannot throw or the end position (vector::insert + // has no effects on failure there). Everything else, including the + // exceptions, is out of line. + if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || + (!copy_cannot_throw(val) && pos.m_it.array_iterator != m_data.m_value.array->end()))) + { + return insert_checked(pos, val); + } + return insert_iterator(pos, val); } @@ -42773,19 +43027,12 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/insert/ iterator insert(const_iterator pos, size_type cnt, const basic_json& val) // NOLINT(performance-unnecessary-value-param) { - // insert only works for arrays - if (JSON_HEDLEY_UNLIKELY(!is_array())) + // see the single-element overload + if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || !copy_cannot_throw(val))) { - throw_insert_wrong_type(*this); + return insert_checked(pos, cnt, val); } - // check if iterator pos fits to this JSON value - if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) - { - throw_iterator_from_other_value(this); - } - - // insert to array and return iterator return insert_iterator(pos, cnt, val); } @@ -42822,8 +43069,7 @@ public: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } - // insert to array and return iterator - return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator); + return insert_range(pos, first.m_it.array_iterator, last.m_it.array_iterator); } /// @brief inserts elements from initializer list into array @@ -42876,8 +43122,34 @@ public: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } - m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); - set_parents(); + // Strong exception safety: range insertion into the container offers + // only the basic guarantee, so insert member by member (as the + // container does) and, if one insertion throws, erase the members this + // call added. + auto& object = *m_data.m_value.object; + if (object.empty()) + { + // all members are new, so the container's own (faster) range + // insertion can be used, and a failure leaves only new members + object_clear_on_failure clear_on_failure(this); + object.insert(first.m_it.object_iterator, last.m_it.object_iterator); + clear_on_failure.commit(); + set_parents(); + return; + } + + object_insert_rollback rollback(this, first.m_it.object_iterator); + for (auto it = first.m_it.object_iterator; it != last.m_it.object_iterator;) + { + rollback.start_chunk(); + for (std::size_t i = 0; i < 64 && it != last.m_it.object_iterator; ++i, ++it) + { + const auto old_size = object.size(); + insert_member(object, *it, detail::priority_tag<1> {}); + rollback.record(object.size() != old_size); + } + } + rollback.commit(); } /// @brief updates a JSON object from another object, overwriting existing keys diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index 4062870d5..fb3209f38 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -12,6 +12,7 @@ #include using nlohmann::json; +#include #include #if JSON_HAS_RANGES #include @@ -804,3 +805,209 @@ TEST_CASE("a failed allocation leaves the value unchanged") #endif } #endif + +// the no-exceptions CI job has no std::bad_alloc to catch; with iterator +// debugging (MSVC STL debug builds) containers allocate inside noexcept +// functions, so failing an allocation there terminates the program +#if !defined(JSON_NOEXCEPTION) && !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0) +namespace +{ +// whether every child of j names j as its parent (always true without +// JSON_DIAGNOSTICS, where no parents are stored) +template +bool children_name_their_parent(const BasicJsonType& j) +{ +#if JSON_DIAGNOSTICS + if (j.is_array()) + { + for (const auto& element : *j.m_data.m_value.array) + { + if (element.m_parent != &j) + { + return false; + } + } + } + else if (j.is_object()) + { + for (const auto& element : *j.m_data.m_value.object) + { + if (element.second.m_parent != &j) + { + return false; + } + } + } +#else + static_cast(j); +#endif + return true; +} + +// Runs op on a fresh value make() with the 0th, 1st, 2nd, ... allocation made +// by the library failing in turn, until op completes. Every failed attempt +// must have thrown std::bad_alloc and left the value exactly as it was, with +// all parent pointers intact (copying a value asserts the invariant). +template +void check_strong_guarantee(const std::function& make, + const std::function& op) +{ + fail_at_alloc_call = -1; + + bool completed = false; + std::size_t failures = 0; + for (std::size_t n = 0; !completed; ++n) + { + CAPTURE(n) + REQUIRE(n < 1000); + + BasicJsonType j = make(); + const std::string before = j.dump(); + + alloc_call_count = 0; + fail_at_alloc_call = static_cast(n); + try + { + op(j); + completed = true; + } + catch (const std::bad_alloc&) + { + ++failures; + } + fail_at_alloc_call = -1; + + if (!completed) + { + CHECK(j.dump() == before); + CHECK(children_name_their_parent(j)); + const BasicJsonType copy(j); + CHECK(copy.dump() == before); + } + } + + // the operation allocates, so at least one attempt must have failed + CHECK(failures > 0); +} + +template +void check_insert_strong_guarantee() +{ + using json_t = BasicJsonType; + using make_t = std::function; + + // strings of this length do not fit into the small string buffer, so + // copying them allocates + const json_t val = std::string(64, 'x'); + const json_t source = json_t::array({std::string(64, 'p'), std::string(64, 'q'), std::string(64, 'r')}); + + for (const bool spare_capacity : + { + false, true + }) + { + CAPTURE(spare_capacity) + + // [ "aaa...", "bbb...", "ccc..." ], with a spare slot if requested + const make_t make = [spare_capacity]() + { + json_t j = json_t::array({std::string(64, 'a'), std::string(64, 'b'), std::string(64, 'c')}); + if (spare_capacity) + { + j.push_back(std::string(64, 'd')); + j.erase(3); + } + return j; + }; + + for (const bool at_end : + { + false, true + }) + { + CAPTURE(at_end) + + SECTION("insert(pos, const basic_json&)") + { + check_strong_guarantee(make, [&](json_t& j) + { + j.insert(at_end ? j.end() : j.begin() + 1, val); + }); + } + + SECTION("insert(pos, basic_json&&)") + { + check_strong_guarantee(make, [&](json_t& j) + { + j.insert(at_end ? j.end() : j.begin() + 1, json_t(val)); + }); + } + + SECTION("insert(pos, size_type, const basic_json&)") + { + check_strong_guarantee(make, [&](json_t& j) + { + j.insert(at_end ? j.end() : j.begin() + 1, 3, val); + }); + } + + SECTION("insert(pos, const_iterator, const_iterator)") + { + check_strong_guarantee(make, [&](json_t& j) + { + j.insert(at_end ? j.end() : j.begin() + 1, source.begin(), source.end()); + }); + } + + SECTION("insert(pos, initializer_list_t)") + { + check_strong_guarantee(make, [&](json_t& j) + { + j.insert(at_end ? j.end() : j.begin() + 1, {val, val}); + }); + } + } + } + + SECTION("insert(const_iterator, const_iterator) for objects") + { + const json_t members = {{"m", std::string(64, 'm')}, {"n", std::string(64, 'n')}, {"o", std::string(64, 'o')}}; + + SECTION("no member exists yet") + { + check_strong_guarantee([]() + { + return json_t({{"a", 1}}); + }, [&](json_t& j) + { + j.insert(members.begin(), members.end()); + }); + } + + SECTION("some members exist already") + { + check_strong_guarantee([]() + { + return json_t({{"a", 1}, {"n", std::string(64, 'x')}}); + }, [&](json_t& j) + { + j.insert(members.begin(), members.end()); + }); + } + } +} +} // namespace + +TEST_CASE("insert() provides the strong exception guarantee (#5986)") +{ + SECTION("std::map-backed object_t") + { + check_insert_strong_guarantee>(); + } + + SECTION("ordered_map-backed object_t") + { + check_insert_strong_guarantee>(); + } +} +#endif