From d1566fe9e8560086763e6f68332d2b48686e3701 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 26 Oct 2026 14:05:38 +0100 Subject: [PATCH] Make the strongly exception-safe pointer operator[] faster than before The previous commit made get_unchecked strongly exception safe but slower with GCC 16 when it creates values (up to +6 % instructions for appending with "-"). This restructures it so that no benchmark row is slower than before the strong guarantee, with GCC 16, Clang 22 and Apple Clang. - get_unchecked walks existing values in one loop that inserts nothing. The last reference token is resolved in that walk: "-" appends with emplace_back(), an index grows the array with operator[], and a missing key is inserted with emplace_hint() at the position the walk's lower_bound() found, so the key is searched once. - With std::less or std::less<> as comparator, the walk checks the key at the lower bound for equality instead of equivalence, which is the same for these comparators and cheaper. - get_unchecked_create (not inlined) handles all other changes. Its guard swaps a null value with a fresh null value instead of copying it and reverts the change in a non-inlined function, so a successful call pays one compare for it. As every null value converted afterwards is a fresh one, it is converted in place instead of assigning a new array or object. - Keys are added to new objects with emplace(), which also replaces the basic_json::operator[] call for ordered_json. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 366 ++++++++++++++-------- single_include/nlohmann/json.hpp | 367 +++++++++++++++-------- 2 files changed, 469 insertions(+), 264 deletions(-) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index f5c0a0db8..42ef1e42a 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -16,6 +16,7 @@ #include // uint64_t #include // strtoull #include // memchr, memcmp, memcpy +#include // less #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO @@ -28,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +136,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType& return array_index_status::ok; } +/// whether two keys of type @a StringType are equivalent under @a Compare +/// exactly if they are equal: std::less and std::less<> +template +struct is_less_comparator : std::false_type {}; + +template +struct is_less_comparator, StringType> : std::true_type {}; + +template +struct is_less_comparator, StringType> : std::true_type {}; + +/// whether the objects of type @a ObjectType find a key of type @a StringType +/// exactly if it equals a stored key +template +struct finds_equal_keys : std::false_type {}; + +template +struct finds_equal_keys> + : conjunction, std::is_same> {}; + } // namespace detail /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document @@ -589,62 +611,184 @@ class json_pointer return object_member_impl(object, key, detail::priority_tag<1> {}); } - /// @brief look up @a key in @a members without inserting it - /// - /// Uses the same search as @ref object_member, so that both compile to the - /// same code for std::map. + /// @brief append a null value to @a array, like operator[] with the size of @a array + template + static BasicJsonType& array_append(BasicJsonType& array) + { + auto& elements = *array.m_data.m_value.array; + const auto old_capacity = array.container_capacity(); + elements.emplace_back(); + return array.set_parent_after_insert(elements.back(), old_capacity); + } + + /// @brief add @a key, which is missing, to @a object + template + static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval()) + { + const auto old_capacity = object.container_capacity(); + const auto it = object.m_data.m_value.object->emplace(key, nullptr).first; + return object.set_parent_after_insert(it->second, old_capacity); + } + + template + static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/) + { + return object.operator[](key); + } + + template + static BasicJsonType& object_add(BasicJsonType& object, const string_t& key) + { + return object_add_impl(object, key, detail::priority_tag<1> {}); + } + + /// @brief search @a key in @a members: the result is where the key is or, + /// for objects that can insert at a position (std::map), where it belongs template - static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/) + static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/) -> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key)) { - const auto it = members.lower_bound(key); - return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end(); + return members.lower_bound(key); } template - static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/) + static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/) -> decltype(members.find(key)) { return members.find(key); } template - static auto object_find(ObjectType& members, const string_t& key) - -> decltype(object_find_impl(members, key, detail::priority_tag<1> {})) + static auto object_search(ObjectType& members, const string_t& key) + -> decltype(object_search_impl(members, key, detail::priority_tag<1> {})) { - return object_find_impl(members, key, detail::priority_tag<1> {}); + return object_search_impl(members, key, detail::priority_tag<1> {}); + } + + /// @brief whether @a it, the result of @ref object_search for @a key, points to @a key + /// + /// With std::less, the key at the lower bound is equivalent to @a key + /// exactly if it is equal to it, and comparing for equality is cheaper. + template::value, int> = 0> + static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/) + -> decltype(members.lower_bound(key), it->first == key) + { + return it != members.end() && it->first == key; + } + + template + static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool()) + { + return it != members.end() && !members.key_comp()(key, it->first); + } + + template + static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/) + { + return it != members.end(); + } + + template + static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key) + { + return object_search_found_impl(members, it, key, detail::priority_tag<2> {}); + } + + /// @brief insert @a key, which is missing, into @a object at @a it, the + /// result of @ref object_search for @a key + template + static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval()) + { + const auto old_capacity = object.container_capacity(); + const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr); + return object.set_parent_after_insert(pos->second, old_capacity); + } + + template + static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/) + { + return object_add(object, key); + } + + template + static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key) + { + return object_insert_impl(object, it, key, detail::priority_tag<1> {}); } /*! @brief undoes the change @ref get_unchecked_create made to an existing value @ref get_unchecked walks existing values without changing them. At the - first reference token it cannot resolve, @ref get_unchecked_create makes - exactly one change to an existing value: it converts a null value, inserts - a key into an object, or appends to an array. Everything it creates - afterwards lies below that change. If a later step throws, the destructor - of this guard reverts that one change, which removes everything created - after it, so the value is left as it was (strong exception safety). The - guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION. + first reference token that does not select an existing value, @ref + get_unchecked_create makes exactly one change to an existing value: it + converts a null value, inserts a key into an object, or grows an array. + Everything it creates afterwards lies below that change. If a later step + throws, the destructor of this guard reverts that one change, which + removes everything created after it, so the value is left as it was + (strong exception safety). The guard needs no try/catch and therefore also + compiles with JSON_NOEXCEPTION. */ template class get_unchecked_rollback { public: - get_unchecked_rollback() = default; + /// record the change that resolving the reference token @a token makes to @a value + get_unchecked_rollback(BasicJsonType* value, const string_t* token) + : m_value(value) + , m_key(token) + { + if (value->is_null()) + { + // keep the null value (with its base class and positions) and + // continue with a fresh null value in its place, which is what + // assigning an array or object to it would have left there + m_saved_null.swap(*value); + m_change = change::null_converted; + } + else if (value->is_object()) + { + m_change = change::key_inserted; + } + else + { + JSON_ASSERT(value->is_array()); + m_old_size = value->m_data.m_value.array->size(); + m_change = change::array_grown; + } + } + get_unchecked_rollback(const get_unchecked_rollback&) = delete; get_unchecked_rollback(get_unchecked_rollback&&) = delete; get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete; get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete; ~get_unchecked_rollback() + { + if (JSON_HEDLEY_UNLIKELY(m_change != change::none)) + { + undo(); + } + } + + /// keep the change + void commit() noexcept + { + m_change = change::none; + } + + private: + JSON_HEDLEY_NEVER_INLINE void undo() noexcept { switch (m_change) { case change::null_converted: - // the copy of the null value also brings back its base - // class and its positions - *m_value = std::move(*m_saved_null); + // the values created since go away with m_saved_null + m_value->swap(m_saved_null); break; case change::key_inserted: m_value->m_data.m_value.object->erase(*m_key); @@ -658,37 +802,6 @@ class json_pointer } } - /// record that the null value @a value, of which @a saved is a copy, is about to be converted - void null_converted(BasicJsonType* value, BasicJsonType* saved) noexcept - { - m_value = value; - m_saved_null = saved; - m_change = change::null_converted; - } - - /// record that @a key, a missing key, is about to be inserted into the object @a value - void key_inserted(BasicJsonType* value, const string_t* key) noexcept - { - m_value = value; - m_key = key; - m_change = change::key_inserted; - } - - /// record that the array @a value is about to grow from @a old_size elements - void array_grown(BasicJsonType* value, typename BasicJsonType::size_type old_size) noexcept - { - m_value = value; - m_old_size = old_size; - m_change = change::array_grown; - } - - /// keep all changes - void commit() noexcept - { - m_change = change::none; - } - - private: enum class change { none, @@ -697,11 +810,11 @@ class json_pointer array_grown }; - change m_change = change::none; - BasicJsonType* m_value = nullptr; - const string_t* m_key = nullptr; + BasicJsonType* m_value; + const string_t* m_key; typename BasicJsonType::size_type m_old_size = 0; - BasicJsonType* m_saved_null = nullptr; + change m_change = change::none; + BasicJsonType m_saved_null {}; }; JSON_PRIVATE_UNLESS_TESTED: @@ -1087,11 +1200,11 @@ class json_pointer Complexity: Linear in the length of the JSON pointer. - Exception safety: Strong guarantee. This function only walks existing - values up to the last reference token. Inserting a key into an object or - growing an array for the last reference token is strongly exception safe - by itself; any other change is made by @ref get_unchecked_create, which - undoes it if it throws. + Exception safety: Strong guarantee. This function walks existing values + until the first reference token that does not select one. If that is the + last reference token, inserting the key into the object or growing the + array is strongly exception safe by itself; any other change is made by + @ref get_unchecked_create, which undoes it if a later step throws. @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number @@ -1100,28 +1213,31 @@ class json_pointer template BasicJsonType& get_unchecked(BasicJsonType* ptr) const { - if (reference_tokens.empty()) + const auto* token = reference_tokens.data(); + const auto* const end = token + reference_tokens.size(); + if (token == end) { return *ptr; } if (ptr->is_null()) { // the null value is converted to an array or an object - return get_unchecked_create(ptr, reference_tokens.data()); + return get_unchecked_create(ptr, token, end); } - // all reference tokens but the last one: walk existing values only - const auto& last_token = reference_tokens.back(); - for (const auto* token = reference_tokens.data(); token != &last_token; ++token) + // walk existing values; the walk itself inserts nothing, which keeps + // the loop fast (insertions are done below or in get_unchecked_create) + typename BasicJsonType::object_t::iterator it; + do { if (ptr->is_object()) { auto& members = *ptr->m_data.m_value.object; - const auto it = object_find(members, *token); - if (it == members.end()) + it = object_search(members, *token); + if (!object_search_found(members, it, *token)) { - // the key is inserted - return get_unchecked_create(ptr, token); + // the key is inserted below or in get_unchecked_create + break; } ptr = &it->second; } @@ -1129,21 +1245,31 @@ class json_pointer { if (is_dash(*token)) { - // an element is appended - return get_unchecked_create(ptr, token); + if (token + 1 == end) + { + // explicitly treat "-" as index beyond the end + return array_append(*ptr); + } + // an element is appended in get_unchecked_create + break; + } + if (token + 1 == end) + { + // convert array index to number; unchecked access + return ptr->operator[](array_index(*token)); } const auto idx = array_index(*token); if (idx >= ptr->m_data.m_value.array->size()) { - // the array is padded with null values - return get_unchecked_create(ptr, token); + // the array is padded in get_unchecked_create + break; } ptr = &(*ptr->m_data.m_value.array)[idx]; } else if (ptr->is_null()) { - // the null value is converted to an array or an object - return get_unchecked_create(ptr, token); + // the null value is converted in get_unchecked_create + break; } else { @@ -1151,82 +1277,49 @@ class json_pointer throw_unresolved(*token, ptr); } } + while (++token != end); - // the last reference token: insert a missing key or grow the array - if (ptr->is_object()) + if (token == end) { - // use unchecked object access - return object_member(*ptr, last_token); + return *ptr; } - if (ptr->is_array()) + if (token + 1 == end && ptr->is_object()) { - // explicitly treat "-" as index beyond the end; otherwise convert - // array index to number; unchecked access - const auto idx = is_dash(last_token) - ? ptr->m_data.m_value.array->size() - : array_index(last_token); - return ptr->operator[](idx); + // insert the missing key where the walk found it belongs + return object_insert(*ptr, it, *token); } - if (ptr->is_null()) - { - // the null value is converted to an array or an object - return get_unchecked_create(ptr, &last_token); - } - // primitive value with a reference token left - throw_unresolved(last_token, ptr); + return get_unchecked_create(ptr, token, end); } /*! @brief create the values for the reference tokens from @a first on Continues @ref get_unchecked at the first reference token that does not - select an existing value. That token changes @a ptr, an existing value - (see @ref get_unchecked_rollback); all later tokens only change values - created here. + select an existing value. That token changes @a ptr, an existing value (see + @ref get_unchecked_rollback); all later tokens only change values created + here. @param[in] ptr the value the reference token @a first is applied to @param[in] first the first reference token to resolve (an element of @ref reference_tokens) + @param[in] end the end of @ref reference_tokens @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number - @throw out_of_range.404 if the JSON pointer can not be resolved */ template - JSON_HEDLEY_NEVER_INLINE BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first) const + JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end) { - if (ptr->is_null()) - { - // the copy keeps the base class and the positions of the null - // value; it must outlive the guard - BasicJsonType saved_null = *ptr; - get_unchecked_rollback rollback; - rollback.null_converted(ptr, &saved_null); - BasicJsonType& result = create_values(ptr, first); - rollback.commit(); - return result; - } - - get_unchecked_rollback rollback; - if (ptr->is_object()) - { - rollback.key_inserted(ptr, first); - } - else - { - JSON_ASSERT(ptr->is_array()); - rollback.array_grown(ptr, ptr->m_data.m_value.array->size()); - } - BasicJsonType& result = create_values(ptr, first); + get_unchecked_rollback rollback(ptr, first); + BasicJsonType& result = create_values(ptr, first, end); rollback.commit(); return result; } /// the loop of @ref get_unchecked_create template - BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first) const + static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end) { - const auto* const end = reference_tokens.data() + reference_tokens.size(); for (const auto* token = first; token != end; ++token) { const auto& reference_token = *token; @@ -1246,16 +1339,25 @@ class json_pointer return std::isdigit(x); }); - // change value to an array for array indices or "-" or to object otherwise - *ptr = (nums || reference_token == "-") - ? detail::value_t::array - : detail::value_t::object; + // change value to an array for array indices or "-" or to an + // object otherwise; every null value here is a fresh one (see + // get_unchecked_rollback), so converting it in place leaves + // the same value as assigning an array or object to it + if (nums || is_dash(reference_token)) + { + ptr->template convert_null_to(); + } + else + { + ptr->template convert_null_to(); + } } if (ptr->is_object()) { - // use unchecked object access - ptr = &object_member(*ptr, reference_token); + // the key is missing: the object is new, or get_unchecked did + // not find the key + ptr = &object_add(*ptr, reference_token); } else { @@ -1265,7 +1367,7 @@ class json_pointer if (is_dash(reference_token)) { // explicitly treat "-" as index beyond the end - ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); + ptr = &array_append(*ptr); } else { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index ac2c66bef..a7c61e889 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -23593,6 +23593,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // uint64_t #include // strtoull #include // memchr, memcmp, memcpy +#include // less #ifndef JSON_NO_IO #include // ostream #endif // JSON_NO_IO @@ -23609,6 +23610,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + // #include // #include @@ -23719,6 +23722,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType& return array_index_status::ok; } +/// whether two keys of type @a StringType are equivalent under @a Compare +/// exactly if they are equal: std::less and std::less<> +template +struct is_less_comparator : std::false_type {}; + +template +struct is_less_comparator, StringType> : std::true_type {}; + +template +struct is_less_comparator, StringType> : std::true_type {}; + +/// whether the objects of type @a ObjectType find a key of type @a StringType +/// exactly if it equals a stored key +template +struct finds_equal_keys : std::false_type {}; + +template +struct finds_equal_keys> + : conjunction, std::is_same> {}; + } // namespace detail /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document @@ -24174,62 +24197,184 @@ class json_pointer return object_member_impl(object, key, detail::priority_tag<1> {}); } - /// @brief look up @a key in @a members without inserting it - /// - /// Uses the same search as @ref object_member, so that both compile to the - /// same code for std::map. + /// @brief append a null value to @a array, like operator[] with the size of @a array + template + static BasicJsonType& array_append(BasicJsonType& array) + { + auto& elements = *array.m_data.m_value.array; + const auto old_capacity = array.container_capacity(); + elements.emplace_back(); + return array.set_parent_after_insert(elements.back(), old_capacity); + } + + /// @brief add @a key, which is missing, to @a object + template + static auto object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace(key, nullptr), std::declval()) + { + const auto old_capacity = object.container_capacity(); + const auto it = object.m_data.m_value.object->emplace(key, nullptr).first; + return object.set_parent_after_insert(it->second, old_capacity); + } + + template + static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/) + { + return object.operator[](key); + } + + template + static BasicJsonType& object_add(BasicJsonType& object, const string_t& key) + { + return object_add_impl(object, key, detail::priority_tag<1> {}); + } + + /// @brief search @a key in @a members: the result is where the key is or, + /// for objects that can insert at a position (std::map), where it belongs template - static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/) + static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<1> /*unused*/) -> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key)) { - const auto it = members.lower_bound(key); - return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end(); + return members.lower_bound(key); } template - static auto object_find_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/) + static auto object_search_impl(ObjectType& members, const string_t& key, detail::priority_tag<0> /*unused*/) -> decltype(members.find(key)) { return members.find(key); } template - static auto object_find(ObjectType& members, const string_t& key) - -> decltype(object_find_impl(members, key, detail::priority_tag<1> {})) + static auto object_search(ObjectType& members, const string_t& key) + -> decltype(object_search_impl(members, key, detail::priority_tag<1> {})) { - return object_find_impl(members, key, detail::priority_tag<1> {}); + return object_search_impl(members, key, detail::priority_tag<1> {}); + } + + /// @brief whether @a it, the result of @ref object_search for @a key, points to @a key + /// + /// With std::less, the key at the lower bound is equivalent to @a key + /// exactly if it is equal to it, and comparing for equality is cheaper. + template::value, int> = 0> + static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<2> /*unused*/) + -> decltype(members.lower_bound(key), it->first == key) + { + return it != members.end() && it->first == key; + } + + template + static auto object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(members.key_comp()(key, it->first), members.lower_bound(key), bool()) + { + return it != members.end() && !members.key_comp()(key, it->first); + } + + template + static bool object_search_found_impl(ObjectType& members, const Iterator& it, const string_t& /*key*/, detail::priority_tag<0> /*unused*/) + { + return it != members.end(); + } + + template + static bool object_search_found(ObjectType& members, const Iterator& it, const string_t& key) + { + return object_search_found_impl(members, it, key, detail::priority_tag<2> {}); + } + + /// @brief insert @a key, which is missing, into @a object at @a it, the + /// result of @ref object_search for @a key + template + static auto object_insert_impl(BasicJsonType& object, const Iterator& it, const string_t& key, detail::priority_tag<1> /*unused*/) + -> decltype(object.m_data.m_value.object->emplace_hint(it, key, nullptr), std::declval()) + { + const auto old_capacity = object.container_capacity(); + const auto pos = object.m_data.m_value.object->emplace_hint(it, key, nullptr); + return object.set_parent_after_insert(pos->second, old_capacity); + } + + template + static BasicJsonType& object_insert_impl(BasicJsonType& object, const Iterator& /*it*/, const string_t& key, detail::priority_tag<0> /*unused*/) + { + return object_add(object, key); + } + + template + static BasicJsonType& object_insert(BasicJsonType& object, const Iterator& it, const string_t& key) + { + return object_insert_impl(object, it, key, detail::priority_tag<1> {}); } /*! @brief undoes the change @ref get_unchecked_create made to an existing value @ref get_unchecked walks existing values without changing them. At the - first reference token it cannot resolve, @ref get_unchecked_create makes - exactly one change to an existing value: it converts a null value, inserts - a key into an object, or appends to an array. Everything it creates - afterwards lies below that change. If a later step throws, the destructor - of this guard reverts that one change, which removes everything created - after it, so the value is left as it was (strong exception safety). The - guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION. + first reference token that does not select an existing value, @ref + get_unchecked_create makes exactly one change to an existing value: it + converts a null value, inserts a key into an object, or grows an array. + Everything it creates afterwards lies below that change. If a later step + throws, the destructor of this guard reverts that one change, which + removes everything created after it, so the value is left as it was + (strong exception safety). The guard needs no try/catch and therefore also + compiles with JSON_NOEXCEPTION. */ template class get_unchecked_rollback { public: - get_unchecked_rollback() = default; + /// record the change that resolving the reference token @a token makes to @a value + get_unchecked_rollback(BasicJsonType* value, const string_t* token) + : m_value(value) + , m_key(token) + { + if (value->is_null()) + { + // keep the null value (with its base class and positions) and + // continue with a fresh null value in its place, which is what + // assigning an array or object to it would have left there + m_saved_null.swap(*value); + m_change = change::null_converted; + } + else if (value->is_object()) + { + m_change = change::key_inserted; + } + else + { + JSON_ASSERT(value->is_array()); + m_old_size = value->m_data.m_value.array->size(); + m_change = change::array_grown; + } + } + get_unchecked_rollback(const get_unchecked_rollback&) = delete; get_unchecked_rollback(get_unchecked_rollback&&) = delete; get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete; get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete; ~get_unchecked_rollback() + { + if (JSON_HEDLEY_UNLIKELY(m_change != change::none)) + { + undo(); + } + } + + /// keep the change + void commit() noexcept + { + m_change = change::none; + } + + private: + JSON_HEDLEY_NEVER_INLINE void undo() noexcept { switch (m_change) { case change::null_converted: - // the copy of the null value also brings back its base - // class and its positions - *m_value = std::move(*m_saved_null); + // the values created since go away with m_saved_null + m_value->swap(m_saved_null); break; case change::key_inserted: m_value->m_data.m_value.object->erase(*m_key); @@ -24243,37 +24388,6 @@ class json_pointer } } - /// record that the null value @a value, of which @a saved is a copy, is about to be converted - void null_converted(BasicJsonType* value, BasicJsonType* saved) noexcept - { - m_value = value; - m_saved_null = saved; - m_change = change::null_converted; - } - - /// record that @a key, a missing key, is about to be inserted into the object @a value - void key_inserted(BasicJsonType* value, const string_t* key) noexcept - { - m_value = value; - m_key = key; - m_change = change::key_inserted; - } - - /// record that the array @a value is about to grow from @a old_size elements - void array_grown(BasicJsonType* value, typename BasicJsonType::size_type old_size) noexcept - { - m_value = value; - m_old_size = old_size; - m_change = change::array_grown; - } - - /// keep all changes - void commit() noexcept - { - m_change = change::none; - } - - private: enum class change { none, @@ -24282,11 +24396,11 @@ class json_pointer array_grown }; - change m_change = change::none; - BasicJsonType* m_value = nullptr; - const string_t* m_key = nullptr; + BasicJsonType* m_value; + const string_t* m_key; typename BasicJsonType::size_type m_old_size = 0; - BasicJsonType* m_saved_null = nullptr; + change m_change = change::none; + BasicJsonType m_saved_null {}; }; JSON_PRIVATE_UNLESS_TESTED: @@ -24672,11 +24786,11 @@ class json_pointer Complexity: Linear in the length of the JSON pointer. - Exception safety: Strong guarantee. This function only walks existing - values up to the last reference token. Inserting a key into an object or - growing an array for the last reference token is strongly exception safe - by itself; any other change is made by @ref get_unchecked_create, which - undoes it if it throws. + Exception safety: Strong guarantee. This function walks existing values + until the first reference token that does not select one. If that is the + last reference token, inserting the key into the object or growing the + array is strongly exception safe by itself; any other change is made by + @ref get_unchecked_create, which undoes it if a later step throws. @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number @@ -24685,28 +24799,31 @@ class json_pointer template BasicJsonType& get_unchecked(BasicJsonType* ptr) const { - if (reference_tokens.empty()) + const auto* token = reference_tokens.data(); + const auto* const end = token + reference_tokens.size(); + if (token == end) { return *ptr; } if (ptr->is_null()) { // the null value is converted to an array or an object - return get_unchecked_create(ptr, reference_tokens.data()); + return get_unchecked_create(ptr, token, end); } - // all reference tokens but the last one: walk existing values only - const auto& last_token = reference_tokens.back(); - for (const auto* token = reference_tokens.data(); token != &last_token; ++token) + // walk existing values; the walk itself inserts nothing, which keeps + // the loop fast (insertions are done below or in get_unchecked_create) + typename BasicJsonType::object_t::iterator it; + do { if (ptr->is_object()) { auto& members = *ptr->m_data.m_value.object; - const auto it = object_find(members, *token); - if (it == members.end()) + it = object_search(members, *token); + if (!object_search_found(members, it, *token)) { - // the key is inserted - return get_unchecked_create(ptr, token); + // the key is inserted below or in get_unchecked_create + break; } ptr = &it->second; } @@ -24714,21 +24831,31 @@ class json_pointer { if (is_dash(*token)) { - // an element is appended - return get_unchecked_create(ptr, token); + if (token + 1 == end) + { + // explicitly treat "-" as index beyond the end + return array_append(*ptr); + } + // an element is appended in get_unchecked_create + break; + } + if (token + 1 == end) + { + // convert array index to number; unchecked access + return ptr->operator[](array_index(*token)); } const auto idx = array_index(*token); if (idx >= ptr->m_data.m_value.array->size()) { - // the array is padded with null values - return get_unchecked_create(ptr, token); + // the array is padded in get_unchecked_create + break; } ptr = &(*ptr->m_data.m_value.array)[idx]; } else if (ptr->is_null()) { - // the null value is converted to an array or an object - return get_unchecked_create(ptr, token); + // the null value is converted in get_unchecked_create + break; } else { @@ -24736,82 +24863,49 @@ class json_pointer throw_unresolved(*token, ptr); } } + while (++token != end); - // the last reference token: insert a missing key or grow the array - if (ptr->is_object()) + if (token == end) { - // use unchecked object access - return object_member(*ptr, last_token); + return *ptr; } - if (ptr->is_array()) + if (token + 1 == end && ptr->is_object()) { - // explicitly treat "-" as index beyond the end; otherwise convert - // array index to number; unchecked access - const auto idx = is_dash(last_token) - ? ptr->m_data.m_value.array->size() - : array_index(last_token); - return ptr->operator[](idx); + // insert the missing key where the walk found it belongs + return object_insert(*ptr, it, *token); } - if (ptr->is_null()) - { - // the null value is converted to an array or an object - return get_unchecked_create(ptr, &last_token); - } - // primitive value with a reference token left - throw_unresolved(last_token, ptr); + return get_unchecked_create(ptr, token, end); } /*! @brief create the values for the reference tokens from @a first on Continues @ref get_unchecked at the first reference token that does not - select an existing value. That token changes @a ptr, an existing value - (see @ref get_unchecked_rollback); all later tokens only change values - created here. + select an existing value. That token changes @a ptr, an existing value (see + @ref get_unchecked_rollback); all later tokens only change values created + here. @param[in] ptr the value the reference token @a first is applied to @param[in] first the first reference token to resolve (an element of @ref reference_tokens) + @param[in] end the end of @ref reference_tokens @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if an array index was not a number - @throw out_of_range.404 if the JSON pointer can not be resolved */ template - JSON_HEDLEY_NEVER_INLINE BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first) const + JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end) { - if (ptr->is_null()) - { - // the copy keeps the base class and the positions of the null - // value; it must outlive the guard - BasicJsonType saved_null = *ptr; - get_unchecked_rollback rollback; - rollback.null_converted(ptr, &saved_null); - BasicJsonType& result = create_values(ptr, first); - rollback.commit(); - return result; - } - - get_unchecked_rollback rollback; - if (ptr->is_object()) - { - rollback.key_inserted(ptr, first); - } - else - { - JSON_ASSERT(ptr->is_array()); - rollback.array_grown(ptr, ptr->m_data.m_value.array->size()); - } - BasicJsonType& result = create_values(ptr, first); + get_unchecked_rollback rollback(ptr, first); + BasicJsonType& result = create_values(ptr, first, end); rollback.commit(); return result; } /// the loop of @ref get_unchecked_create template - BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first) const + static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end) { - const auto* const end = reference_tokens.data() + reference_tokens.size(); for (const auto* token = first; token != end; ++token) { const auto& reference_token = *token; @@ -24831,16 +24925,25 @@ class json_pointer return std::isdigit(x); }); - // change value to an array for array indices or "-" or to object otherwise - *ptr = (nums || reference_token == "-") - ? detail::value_t::array - : detail::value_t::object; + // change value to an array for array indices or "-" or to an + // object otherwise; every null value here is a fresh one (see + // get_unchecked_rollback), so converting it in place leaves + // the same value as assigning an array or object to it + if (nums || is_dash(reference_token)) + { + ptr->template convert_null_to(); + } + else + { + ptr->template convert_null_to(); + } } if (ptr->is_object()) { - // use unchecked object access - ptr = &object_member(*ptr, reference_token); + // the key is missing: the object is new, or get_unchecked did + // not find the key + ptr = &object_add(*ptr, reference_token); } else { @@ -24850,7 +24953,7 @@ class json_pointer if (is_dash(reference_token)) { // explicitly treat "-" as index beyond the end - ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); + ptr = &array_append(*ptr); } else {