From 224116130769bf41f1b65e51802577effe8b36b5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 2 Nov 2026 08:54:34 +0100 Subject: [PATCH] insert(): strong guarantee for single elements, basic for the rest Rework of the previous two commits, following the discussion in #5996: the standard containers give the strong guarantee only for a single element (std::vector: at the end, or if the exception does not come from copying T; std::map: single-element insert), and copying a basic_json allocates. Rebuilding a rollback for the count, range and object-range overloads on top of array_t/object_t made the code verbose and slower. - insert(pos, const basic_json&): at end() forward as before (the standard covers it); elsewhere copy first and insert by move, as libstdc++ does internally (libc++ shifts the tail before copying). - insert_iterator() and object insert(first, last) set the parents in a scope guard, so a failed insertion leaves a valid value with JSON_DIAGNOSTICS too (the next assert_invariant() aborted before). - Docs: strong guarantee for (1), basic guarantee for (2)-(5); the argument checks throw before anything is changed. - Test: allocation-failure sweep checks the strong guarantee for (1) and validity, type and parents for the other overloads. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/insert.md | 14 +- include/nlohmann/json.hpp | 372 ++++------------------ single_include/nlohmann/json.hpp | 372 ++++------------------ tests/src/unit-allocator.cpp | 47 ++- 4 files changed, 157 insertions(+), 648 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/insert.md b/docs/mkdocs/docs/api/basic_json/insert.md index 992010010..adf4bcf26 100644 --- a/docs/mkdocs/docs/api/basic_json/insert.md +++ b/docs/mkdocs/docs/api/basic_json/insert.md @@ -65,9 +65,17 @@ the same index, which is now a different value. ## Exception safety -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. +1. Strong guarantee: if an exception is thrown, there are no changes in the JSON value. +2. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON + value may be partially modified, but it stays a valid array. +3. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON + value may be partially modified, but it stays a valid array. +4. Basic guarantee: if an exception is thrown while the elements are copied (for example, `std::bad_alloc`), the JSON + value may be partially modified, but it stays a valid array. +5. Basic guarantee: if an exception is thrown while the members are copied (for example, `std::bad_alloc`), the JSON + value may be partially modified, but it stays a valid object. + +The exceptions listed below are thrown before the JSON value is changed. ## Exceptions diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 91bb663c5..b2d46d57f 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -4914,6 +4914,30 @@ public: return {it, res.second}; } + private: + /// Sets the parents of a value when it goes out of scope, also when an + /// exception is thrown: a container insertion that throws may already + /// have added elements, whose parents must be set for the value to stay + /// valid (basic exception guarantee). Does nothing without JSON_DIAGNOSTICS. + class set_parents_on_exit + { + public: + explicit set_parents_on_exit(basic_json* v) noexcept : m_value(v) {} + set_parents_on_exit(const set_parents_on_exit&) = delete; + set_parents_on_exit& operator=(const set_parents_on_exit&) = delete; + set_parents_on_exit(set_parents_on_exit&&) = delete; + set_parents_on_exit& operator=(set_parents_on_exit&&) = delete; + + ~set_parents_on_exit() + { + m_value->set_parents(); + } + + private: + basic_json* m_value; + }; + + public: /// Helper for insertion of an iterator /// @note: This uses std::distance to support GCC 4.8, /// see https://github.com/nlohmann/json/pull/1257 @@ -4923,6 +4947,7 @@ public: iterator result(this); JSON_ASSERT(m_data.m_value.array != nullptr); + const set_parents_on_exit parents(this); auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator); m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward(args)...); result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos; @@ -4931,298 +4956,35 @@ public: // result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val); // but the return value of insert is missing in GCC 4.8, so it is written this way instead. - set_parents(); return result; } - 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; - array_append_rollback(array_append_rollback&&) = delete; - array_append_rollback& operator=(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())) - { - 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); - } - - 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; - object_clear_on_failure(object_clear_on_failure&&) = delete; - object_clear_on_failure& operator=(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; - object_insert_rollback(object_insert_rollback&&) = delete; - object_insert_rollback& operator=(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 = logged_word(chunk); - if (((word >> (i % 64)) & 1U) != 0) - { - object.erase(it->first); - } - } - } - m_value->set_parents(); - } - -private: - static constexpr std::size_t inline_words = 4; - - /// the log word of a chunk: the one being filled, an inline one, or an overflow one - std::uint64_t logged_word(std::size_t chunk) const - { - if (chunk >= m_flushed) - { - return m_word; - } - return chunk < inline_words ? m_inline[chunk] : m_more[chunk - inline_words]; - } - - 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::array m_inline {}; - 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()))) + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - return insert_checked(pos, val); + throw_insert_wrong_type(*this); } - return insert_iterator(pos, val); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // vector::insert has no effects if copying a single element to the + // end throws; elsewhere, it may already have shifted the elements, so + // copy first and insert by move, which cannot throw but by allocating + if (pos.m_it.array_iterator == m_data.m_value.array->end()) + { + return insert_iterator(pos, val); + } + + basic_json tmp(val); + return insert_iterator(pos, std::move(tmp)); } /// @brief inserts element into array @@ -5251,12 +5013,19 @@ private: /// @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) { - // see the single-element overload - if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || !copy_cannot_throw(val))) + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - return insert_checked(pos, cnt, val); + 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); + } + + // insert to array and return iterator return insert_iterator(pos, cnt, val); } @@ -5293,7 +5062,8 @@ private: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } - return insert_range(pos, first.m_it.array_iterator, last.m_it.array_iterator); + // insert to array and return iterator + return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator); } /// @brief inserts elements from initializer list into array @@ -5346,34 +5116,8 @@ private: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } - // 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(); + const set_parents_on_exit parents(this); + m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); } /// @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 ef4b94c4f..cce98b467 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -42706,6 +42706,30 @@ public: return {it, res.second}; } + private: + /// Sets the parents of a value when it goes out of scope, also when an + /// exception is thrown: a container insertion that throws may already + /// have added elements, whose parents must be set for the value to stay + /// valid (basic exception guarantee). Does nothing without JSON_DIAGNOSTICS. + class set_parents_on_exit + { + public: + explicit set_parents_on_exit(basic_json* v) noexcept : m_value(v) {} + set_parents_on_exit(const set_parents_on_exit&) = delete; + set_parents_on_exit& operator=(const set_parents_on_exit&) = delete; + set_parents_on_exit(set_parents_on_exit&&) = delete; + set_parents_on_exit& operator=(set_parents_on_exit&&) = delete; + + ~set_parents_on_exit() + { + m_value->set_parents(); + } + + private: + basic_json* m_value; + }; + + public: /// Helper for insertion of an iterator /// @note: This uses std::distance to support GCC 4.8, /// see https://github.com/nlohmann/json/pull/1257 @@ -42715,6 +42739,7 @@ public: iterator result(this); JSON_ASSERT(m_data.m_value.array != nullptr); + const set_parents_on_exit parents(this); auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator); m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward(args)...); result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos; @@ -42723,298 +42748,35 @@ public: // result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val); // but the return value of insert is missing in GCC 4.8, so it is written this way instead. - set_parents(); return result; } - 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; - array_append_rollback(array_append_rollback&&) = delete; - array_append_rollback& operator=(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())) - { - 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); - } - - 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; - object_clear_on_failure(object_clear_on_failure&&) = delete; - object_clear_on_failure& operator=(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; - object_insert_rollback(object_insert_rollback&&) = delete; - object_insert_rollback& operator=(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 = logged_word(chunk); - if (((word >> (i % 64)) & 1U) != 0) - { - object.erase(it->first); - } - } - } - m_value->set_parents(); - } - -private: - static constexpr std::size_t inline_words = 4; - - /// the log word of a chunk: the one being filled, an inline one, or an overflow one - std::uint64_t logged_word(std::size_t chunk) const - { - if (chunk >= m_flushed) - { - return m_word; - } - return chunk < inline_words ? m_inline[chunk] : m_more[chunk - inline_words]; - } - - 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::array m_inline {}; - 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()))) + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - return insert_checked(pos, val); + throw_insert_wrong_type(*this); } - return insert_iterator(pos, val); + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + throw_iterator_from_other_value(this); + } + + // vector::insert has no effects if copying a single element to the + // end throws; elsewhere, it may already have shifted the elements, so + // copy first and insert by move, which cannot throw but by allocating + if (pos.m_it.array_iterator == m_data.m_value.array->end()) + { + return insert_iterator(pos, val); + } + + basic_json tmp(val); + return insert_iterator(pos, std::move(tmp)); } /// @brief inserts element into array @@ -43043,12 +42805,19 @@ private: /// @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) { - // see the single-element overload - if (JSON_HEDLEY_UNLIKELY(!is_array() || pos.m_object != this || !copy_cannot_throw(val))) + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) { - return insert_checked(pos, cnt, val); + 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); + } + + // insert to array and return iterator return insert_iterator(pos, cnt, val); } @@ -43085,7 +42854,8 @@ private: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to arrays", this); } - return insert_range(pos, first.m_it.array_iterator, last.m_it.array_iterator); + // insert to array and return iterator + return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator); } /// @brief inserts elements from initializer list into array @@ -43138,34 +42908,8 @@ private: detail::throw_literal(detail::exception_id::iterator_from_other_value, "iterators first and last must point to objects", this); } - // 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(); + const set_parents_on_exit parents(this); + m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); } /// @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 5e0997d20..02b145aae 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -844,13 +844,21 @@ bool children_name_their_parent(const BasicJsonType& j) return true; } +enum class guarantee +{ + basic, // the value stays valid and keeps its type + strong // the value is unchanged +}; + // 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). +// must have thrown std::bad_alloc and left the value valid, with all parent +// pointers intact (copying a value asserts the invariant), and for the strong +// guarantee exactly as it was. template -void check_strong_guarantee(const std::function& make, - const std::function& op) +void check_exception_guarantee(guarantee g, + const std::function& make, + const std::function& op) { fail_at_alloc_call = -1; @@ -863,6 +871,7 @@ void check_strong_guarantee(const std::function& make, BasicJsonType j = make(); const std::string before = j.dump(); + const auto type_before = j.type(); alloc_call_count = 0; fail_at_alloc_call = static_cast(n); @@ -879,10 +888,14 @@ void check_strong_guarantee(const std::function& make, if (!completed) { - CHECK(j.dump() == before); + CHECK((j.type() == type_before)); CHECK(children_name_their_parent(j)); const BasicJsonType copy(j); // NOLINT(performance-unnecessary-copy-initialization): the copy is the point, it allocates - CHECK(copy.dump() == before); + CHECK(copy.dump() == j.dump()); + if (g == guarantee::strong) + { + CHECK(j.dump() == before); + } } } @@ -891,7 +904,7 @@ void check_strong_guarantee(const std::function& make, } template -void check_insert_strong_guarantee() +void check_insert_exception_guarantees() { using json_t = BasicJsonType; using make_t = std::function; @@ -929,7 +942,7 @@ void check_insert_strong_guarantee() SECTION("insert(pos, const basic_json&)") { - check_strong_guarantee(make, [&](json_t& j) + check_exception_guarantee(guarantee::strong, make, [&](json_t& j) { j.insert(at_end ? j.end() : j.begin() + 1, val); }); @@ -937,7 +950,7 @@ void check_insert_strong_guarantee() SECTION("insert(pos, basic_json&&)") { - check_strong_guarantee(make, [&](json_t& j) + check_exception_guarantee(guarantee::strong, make, [&](json_t& j) { j.insert(at_end ? j.end() : j.begin() + 1, json_t(val)); }); @@ -945,7 +958,7 @@ void check_insert_strong_guarantee() SECTION("insert(pos, size_type, const basic_json&)") { - check_strong_guarantee(make, [&](json_t& j) + check_exception_guarantee(guarantee::basic, make, [&](json_t& j) { j.insert(at_end ? j.end() : j.begin() + 1, 3, val); }); @@ -953,7 +966,7 @@ void check_insert_strong_guarantee() SECTION("insert(pos, const_iterator, const_iterator)") { - check_strong_guarantee(make, [&](json_t& j) + check_exception_guarantee(guarantee::basic, make, [&](json_t& j) { j.insert(at_end ? j.end() : j.begin() + 1, source.begin(), source.end()); }); @@ -961,7 +974,7 @@ void check_insert_strong_guarantee() SECTION("insert(pos, initializer_list_t)") { - check_strong_guarantee(make, [&](json_t& j) + check_exception_guarantee(guarantee::basic, make, [&](json_t& j) { j.insert(at_end ? j.end() : j.begin() + 1, {val, val}); }); @@ -975,7 +988,7 @@ void check_insert_strong_guarantee() SECTION("no member exists yet") { - check_strong_guarantee([]() + check_exception_guarantee(guarantee::basic, []() { return json_t({{"a", 1}}); }, [&](json_t& j) @@ -986,7 +999,7 @@ void check_insert_strong_guarantee() SECTION("some members exist already") { - check_strong_guarantee([]() + check_exception_guarantee(guarantee::basic, []() { return json_t({{"a", 1}, {"n", std::string(64, 'x')}}); }, [&](json_t& j) @@ -998,16 +1011,16 @@ void check_insert_strong_guarantee() } } // namespace -TEST_CASE("insert() provides the strong exception guarantee (#5986)") +TEST_CASE("insert() provides its documented exception guarantees (#5986)") { SECTION("std::map-backed object_t") { - check_insert_strong_guarantee>(); + check_insert_exception_guarantees>(); } SECTION("ordered_map-backed object_t") { - check_insert_strong_guarantee>(); + check_insert_exception_guarantees>(); } } #endif