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<string_t> 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 <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-26 17:13:52 +01:00
1 parent 3351a8795b
commit d1566fe9e8
2 files changed
+469 -264

No files matched your search

+234 -132
View File
@@ -16,6 +16,7 @@
#include <cstdint> // uint64_t #include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy #include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
@@ -28,6 +29,7 @@
#include <nlohmann/detail/macro_scope.hpp> #include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp> #include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/type_traits.hpp> #include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/meta/void_t.hpp>
#include <nlohmann/detail/string_concat.hpp> #include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp> #include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/string_utils.hpp> #include <nlohmann/detail/string_utils.hpp>
@@ -134,6 +136,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok; return array_index_status::ok;
} }
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, 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<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail } // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @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> {}); return object_member_impl(object, key, detail::priority_tag<1> {});
} }
/// @brief look up @a key in @a members without inserting it /// @brief append a null value to @a array, like operator[] with the size of @a array
/// template<typename BasicJsonType>
/// Uses the same search as @ref object_member, so that both compile to the static BasicJsonType& array_append(BasicJsonType& array)
/// same code for std::map. {
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<typename BasicJsonType>
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<BasicJsonType&>())
{
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<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
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<typename ObjectType> template<typename ObjectType>
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)) -> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{ {
const auto it = members.lower_bound(key); return members.lower_bound(key);
return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end();
} }
template<typename ObjectType> template<typename ObjectType>
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)) -> decltype(members.find(key))
{ {
return members.find(key); return members.find(key);
} }
template<typename ObjectType> template<typename ObjectType>
static auto object_find(ObjectType& members, const string_t& key) static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_find_impl(members, key, detail::priority_tag<1> {})) -> 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<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::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<typename ObjectType, typename Iterator>
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<typename ObjectType, typename Iterator>
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<typename ObjectType, typename Iterator>
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<typename BasicJsonType, typename Iterator>
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<BasicJsonType&>())
{
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<typename BasicJsonType, typename Iterator>
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<typename BasicJsonType, typename Iterator>
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 @brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the @ref get_unchecked walks existing values without changing them. At the
first reference token it cannot resolve, @ref get_unchecked_create makes first reference token that does not select an existing value, @ref
exactly one change to an existing value: it converts a null value, inserts get_unchecked_create makes exactly one change to an existing value: it
a key into an object, or appends to an array. Everything it creates converts a null value, inserts a key into an object, or grows an array.
afterwards lies below that change. If a later step throws, the destructor Everything it creates afterwards lies below that change. If a later step
of this guard reverts that one change, which removes everything created throws, the destructor of this guard reverts that one change, which
after it, so the value is left as it was (strong exception safety). The removes everything created after it, so the value is left as it was
guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION. (strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
class get_unchecked_rollback class get_unchecked_rollback
{ {
public: 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(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete; get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete; get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete; get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback() ~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) switch (m_change)
{ {
case change::null_converted: case change::null_converted:
// the copy of the null value also brings back its base // the values created since go away with m_saved_null
// class and its positions m_value->swap(m_saved_null);
*m_value = std::move(*m_saved_null);
break; break;
case change::key_inserted: case change::key_inserted:
m_value->m_data.m_value.object->erase(*m_key); 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 enum class change
{ {
none, none,
@@ -697,11 +810,11 @@ class json_pointer
array_grown array_grown
}; };
change m_change = change::none; BasicJsonType* m_value;
BasicJsonType* m_value = nullptr; const string_t* m_key;
const string_t* m_key = nullptr;
typename BasicJsonType::size_type m_old_size = 0; 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: JSON_PRIVATE_UNLESS_TESTED:
@@ -1087,11 +1200,11 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer. Complexity: Linear in the length of the JSON pointer.
Exception safety: Strong guarantee. This function only walks existing Exception safety: Strong guarantee. This function walks existing values
values up to the last reference token. Inserting a key into an object or until the first reference token that does not select one. If that is the
growing an array for the last reference token is strongly exception safe last reference token, inserting the key into the object or growing the
by itself; any other change is made by @ref get_unchecked_create, which array is strongly exception safe by itself; any other change is made by
undoes it if it throws. @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.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@@ -1100,28 +1213,31 @@ class json_pointer
template<typename BasicJsonType> template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const 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; return *ptr;
} }
if (ptr->is_null()) if (ptr->is_null())
{ {
// the null value is converted to an array or an object // 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 // walk existing values; the walk itself inserts nothing, which keeps
const auto& last_token = reference_tokens.back(); // the loop fast (insertions are done below or in get_unchecked_create)
for (const auto* token = reference_tokens.data(); token != &last_token; ++token) typename BasicJsonType::object_t::iterator it;
do
{ {
if (ptr->is_object()) if (ptr->is_object())
{ {
auto& members = *ptr->m_data.m_value.object; auto& members = *ptr->m_data.m_value.object;
const auto it = object_find(members, *token); it = object_search(members, *token);
if (it == members.end()) if (!object_search_found(members, it, *token))
{ {
// the key is inserted // the key is inserted below or in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
ptr = &it->second; ptr = &it->second;
} }
@@ -1129,21 +1245,31 @@ class json_pointer
{ {
if (is_dash(*token)) if (is_dash(*token))
{ {
// an element is appended if (token + 1 == end)
return get_unchecked_create(ptr, token); {
// 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<BasicJsonType>(*token));
} }
const auto idx = array_index<BasicJsonType>(*token); const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size()) if (idx >= ptr->m_data.m_value.array->size())
{ {
// the array is padded with null values // the array is padded in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
ptr = &(*ptr->m_data.m_value.array)[idx]; ptr = &(*ptr->m_data.m_value.array)[idx];
} }
else if (ptr->is_null()) else if (ptr->is_null())
{ {
// the null value is converted to an array or an object // the null value is converted in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
else else
{ {
@@ -1151,82 +1277,49 @@ class json_pointer
throw_unresolved(*token, ptr); throw_unresolved(*token, ptr);
} }
} }
while (++token != end);
// the last reference token: insert a missing key or grow the array if (token == end)
if (ptr->is_object())
{ {
// use unchecked object access return *ptr;
return object_member(*ptr, last_token);
} }
if (ptr->is_array()) if (token + 1 == end && ptr->is_object())
{ {
// explicitly treat "-" as index beyond the end; otherwise convert // insert the missing key where the walk found it belongs
// array index to number; unchecked access return object_insert(*ptr, it, *token);
const auto idx = is_dash(last_token)
? ptr->m_data.m_value.array->size()
: array_index<BasicJsonType>(last_token);
return ptr->operator[](idx);
} }
if (ptr->is_null()) return get_unchecked_create(ptr, token, end);
{
// 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);
} }
/*! /*!
@brief create the values for the reference tokens from @a first on @brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value select an existing value. That token changes @a ptr, an existing value (see
(see @ref get_unchecked_rollback); all later tokens only change values @ref get_unchecked_rollback); all later tokens only change values created
created here. here.
@param[in] ptr the value the reference token @a first is applied to @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 @param[in] first the first reference token to resolve (an element of
@ref reference_tokens) @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.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @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<typename BasicJsonType> template<typename BasicJsonType>
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()) get_unchecked_rollback<BasicJsonType> rollback(ptr, first);
{ BasicJsonType& result = create_values(ptr, first, end);
// 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<BasicJsonType> rollback;
rollback.null_converted(ptr, &saved_null);
BasicJsonType& result = create_values(ptr, first);
rollback.commit();
return result;
}
get_unchecked_rollback<BasicJsonType> 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);
rollback.commit(); rollback.commit();
return result; return result;
} }
/// the loop of @ref get_unchecked_create /// the loop of @ref get_unchecked_create
template<typename BasicJsonType> template<typename BasicJsonType>
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) for (const auto* token = first; token != end; ++token)
{ {
const auto& reference_token = *token; const auto& reference_token = *token;
@@ -1246,16 +1339,25 @@ class json_pointer
return std::isdigit(x); return std::isdigit(x);
}); });
// change value to an array for array indices or "-" or to object otherwise // change value to an array for array indices or "-" or to an
*ptr = (nums || reference_token == "-") // object otherwise; every null value here is a fresh one (see
? detail::value_t::array // get_unchecked_rollback), so converting it in place leaves
: detail::value_t::object; // the same value as assigning an array or object to it
if (nums || is_dash(reference_token))
{
ptr->template convert_null_to<typename BasicJsonType::array_t>();
}
else
{
ptr->template convert_null_to<typename BasicJsonType::object_t>();
}
} }
if (ptr->is_object()) if (ptr->is_object())
{ {
// use unchecked object access // the key is missing: the object is new, or get_unchecked did
ptr = &object_member(*ptr, reference_token); // not find the key
ptr = &object_add(*ptr, reference_token);
} }
else else
{ {
@@ -1265,7 +1367,7 @@ class json_pointer
if (is_dash(reference_token)) if (is_dash(reference_token))
{ {
// explicitly treat "-" as index beyond the end // explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); ptr = &array_append(*ptr);
} }
else else
{ {
+235 -132
View File
@@ -23593,6 +23593,7 @@ NLOHMANN_JSON_NAMESPACE_END
#include <cstdint> // uint64_t #include <cstdint> // uint64_t
#include <cstdlib> // strtoull #include <cstdlib> // strtoull
#include <cstring> // memchr, memcmp, memcpy #include <cstring> // memchr, memcmp, memcpy
#include <functional> // less
#ifndef JSON_NO_IO #ifndef JSON_NO_IO
#include <iosfwd> // ostream #include <iosfwd> // ostream
#endif // JSON_NO_IO #endif // JSON_NO_IO
@@ -23609,6 +23610,8 @@ NLOHMANN_JSON_NAMESPACE_END
// #include <nlohmann/detail/meta/type_traits.hpp> // #include <nlohmann/detail/meta/type_traits.hpp>
// #include <nlohmann/detail/meta/void_t.hpp>
// #include <nlohmann/detail/string_concat.hpp> // #include <nlohmann/detail/string_concat.hpp>
// #include <nlohmann/detail/string_escape.hpp> // #include <nlohmann/detail/string_escape.hpp>
@@ -23719,6 +23722,26 @@ JSON_HEDLEY_ALWAYS_INLINE array_index_status parse_array_index(const StringType&
return array_index_status::ok; return array_index_status::ok;
} }
/// whether two keys of type @a StringType are equivalent under @a Compare
/// exactly if they are equal: std::less<StringType> and std::less<>
template<typename Compare, typename StringType>
struct is_less_comparator : std::false_type {};
template<typename StringType>
struct is_less_comparator<std::less<StringType>, StringType> : std::true_type {};
template<typename StringType>
struct is_less_comparator<std::less<void>, 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<typename ObjectType, typename StringType, typename = void>
struct finds_equal_keys : std::false_type {};
template<typename ObjectType, typename StringType>
struct finds_equal_keys<ObjectType, StringType, void_t<typename ObjectType::key_compare, typename ObjectType::key_type>>
: conjunction<is_less_comparator<typename ObjectType::key_compare, StringType>, std::is_same<typename ObjectType::key_type, StringType>> {};
} // namespace detail } // namespace detail
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document /// @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> {}); return object_member_impl(object, key, detail::priority_tag<1> {});
} }
/// @brief look up @a key in @a members without inserting it /// @brief append a null value to @a array, like operator[] with the size of @a array
/// template<typename BasicJsonType>
/// Uses the same search as @ref object_member, so that both compile to the static BasicJsonType& array_append(BasicJsonType& array)
/// same code for std::map. {
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<typename BasicJsonType>
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<BasicJsonType&>())
{
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<typename BasicJsonType>
static BasicJsonType& object_add_impl(BasicJsonType& object, const string_t& key, detail::priority_tag<0> /*unused*/)
{
return object.operator[](key);
}
template<typename BasicJsonType>
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<typename ObjectType> template<typename ObjectType>
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)) -> decltype(members.key_comp()(key, members.lower_bound(key)->first), members.lower_bound(key))
{ {
const auto it = members.lower_bound(key); return members.lower_bound(key);
return (it != members.end() && !members.key_comp()(key, it->first)) ? it : members.end();
} }
template<typename ObjectType> template<typename ObjectType>
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)) -> decltype(members.find(key))
{ {
return members.find(key); return members.find(key);
} }
template<typename ObjectType> template<typename ObjectType>
static auto object_find(ObjectType& members, const string_t& key) static auto object_search(ObjectType& members, const string_t& key)
-> decltype(object_find_impl(members, key, detail::priority_tag<1> {})) -> 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<typename ObjectType, typename Iterator,
detail::enable_if_t<detail::finds_equal_keys<ObjectType, string_t>::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<typename ObjectType, typename Iterator>
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<typename ObjectType, typename Iterator>
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<typename ObjectType, typename Iterator>
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<typename BasicJsonType, typename Iterator>
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<BasicJsonType&>())
{
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<typename BasicJsonType, typename Iterator>
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<typename BasicJsonType, typename Iterator>
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 @brief undoes the change @ref get_unchecked_create made to an existing value
@ref get_unchecked walks existing values without changing them. At the @ref get_unchecked walks existing values without changing them. At the
first reference token it cannot resolve, @ref get_unchecked_create makes first reference token that does not select an existing value, @ref
exactly one change to an existing value: it converts a null value, inserts get_unchecked_create makes exactly one change to an existing value: it
a key into an object, or appends to an array. Everything it creates converts a null value, inserts a key into an object, or grows an array.
afterwards lies below that change. If a later step throws, the destructor Everything it creates afterwards lies below that change. If a later step
of this guard reverts that one change, which removes everything created throws, the destructor of this guard reverts that one change, which
after it, so the value is left as it was (strong exception safety). The removes everything created after it, so the value is left as it was
guard needs no try/catch and therefore also compiles with JSON_NOEXCEPTION. (strong exception safety). The guard needs no try/catch and therefore also
compiles with JSON_NOEXCEPTION.
*/ */
template<typename BasicJsonType> template<typename BasicJsonType>
class get_unchecked_rollback class get_unchecked_rollback
{ {
public: 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(const get_unchecked_rollback&) = delete;
get_unchecked_rollback(get_unchecked_rollback&&) = delete; get_unchecked_rollback(get_unchecked_rollback&&) = delete;
get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete; get_unchecked_rollback& operator=(const get_unchecked_rollback&) = delete;
get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete; get_unchecked_rollback& operator=(get_unchecked_rollback&&) = delete;
~get_unchecked_rollback() ~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) switch (m_change)
{ {
case change::null_converted: case change::null_converted:
// the copy of the null value also brings back its base // the values created since go away with m_saved_null
// class and its positions m_value->swap(m_saved_null);
*m_value = std::move(*m_saved_null);
break; break;
case change::key_inserted: case change::key_inserted:
m_value->m_data.m_value.object->erase(*m_key); 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 enum class change
{ {
none, none,
@@ -24282,11 +24396,11 @@ class json_pointer
array_grown array_grown
}; };
change m_change = change::none; BasicJsonType* m_value;
BasicJsonType* m_value = nullptr; const string_t* m_key;
const string_t* m_key = nullptr;
typename BasicJsonType::size_type m_old_size = 0; 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: JSON_PRIVATE_UNLESS_TESTED:
@@ -24672,11 +24786,11 @@ class json_pointer
Complexity: Linear in the length of the JSON pointer. Complexity: Linear in the length of the JSON pointer.
Exception safety: Strong guarantee. This function only walks existing Exception safety: Strong guarantee. This function walks existing values
values up to the last reference token. Inserting a key into an object or until the first reference token that does not select one. If that is the
growing an array for the last reference token is strongly exception safe last reference token, inserting the key into the object or growing the
by itself; any other change is made by @ref get_unchecked_create, which array is strongly exception safe by itself; any other change is made by
undoes it if it throws. @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.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@@ -24685,28 +24799,31 @@ class json_pointer
template<typename BasicJsonType> template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const 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; return *ptr;
} }
if (ptr->is_null()) if (ptr->is_null())
{ {
// the null value is converted to an array or an object // 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 // walk existing values; the walk itself inserts nothing, which keeps
const auto& last_token = reference_tokens.back(); // the loop fast (insertions are done below or in get_unchecked_create)
for (const auto* token = reference_tokens.data(); token != &last_token; ++token) typename BasicJsonType::object_t::iterator it;
do
{ {
if (ptr->is_object()) if (ptr->is_object())
{ {
auto& members = *ptr->m_data.m_value.object; auto& members = *ptr->m_data.m_value.object;
const auto it = object_find(members, *token); it = object_search(members, *token);
if (it == members.end()) if (!object_search_found(members, it, *token))
{ {
// the key is inserted // the key is inserted below or in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
ptr = &it->second; ptr = &it->second;
} }
@@ -24714,21 +24831,31 @@ class json_pointer
{ {
if (is_dash(*token)) if (is_dash(*token))
{ {
// an element is appended if (token + 1 == end)
return get_unchecked_create(ptr, token); {
// 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<BasicJsonType>(*token));
} }
const auto idx = array_index<BasicJsonType>(*token); const auto idx = array_index<BasicJsonType>(*token);
if (idx >= ptr->m_data.m_value.array->size()) if (idx >= ptr->m_data.m_value.array->size())
{ {
// the array is padded with null values // the array is padded in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
ptr = &(*ptr->m_data.m_value.array)[idx]; ptr = &(*ptr->m_data.m_value.array)[idx];
} }
else if (ptr->is_null()) else if (ptr->is_null())
{ {
// the null value is converted to an array or an object // the null value is converted in get_unchecked_create
return get_unchecked_create(ptr, token); break;
} }
else else
{ {
@@ -24736,82 +24863,49 @@ class json_pointer
throw_unresolved(*token, ptr); throw_unresolved(*token, ptr);
} }
} }
while (++token != end);
// the last reference token: insert a missing key or grow the array if (token == end)
if (ptr->is_object())
{ {
// use unchecked object access return *ptr;
return object_member(*ptr, last_token);
} }
if (ptr->is_array()) if (token + 1 == end && ptr->is_object())
{ {
// explicitly treat "-" as index beyond the end; otherwise convert // insert the missing key where the walk found it belongs
// array index to number; unchecked access return object_insert(*ptr, it, *token);
const auto idx = is_dash(last_token)
? ptr->m_data.m_value.array->size()
: array_index<BasicJsonType>(last_token);
return ptr->operator[](idx);
} }
if (ptr->is_null()) return get_unchecked_create(ptr, token, end);
{
// 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);
} }
/*! /*!
@brief create the values for the reference tokens from @a first on @brief create the values for the reference tokens from @a first on
Continues @ref get_unchecked at the first reference token that does not Continues @ref get_unchecked at the first reference token that does not
select an existing value. That token changes @a ptr, an existing value select an existing value. That token changes @a ptr, an existing value (see
(see @ref get_unchecked_rollback); all later tokens only change values @ref get_unchecked_rollback); all later tokens only change values created
created here. here.
@param[in] ptr the value the reference token @a first is applied to @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 @param[in] first the first reference token to resolve (an element of
@ref reference_tokens) @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.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @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<typename BasicJsonType> template<typename BasicJsonType>
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()) get_unchecked_rollback<BasicJsonType> rollback(ptr, first);
{ BasicJsonType& result = create_values(ptr, first, end);
// 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<BasicJsonType> rollback;
rollback.null_converted(ptr, &saved_null);
BasicJsonType& result = create_values(ptr, first);
rollback.commit();
return result;
}
get_unchecked_rollback<BasicJsonType> 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);
rollback.commit(); rollback.commit();
return result; return result;
} }
/// the loop of @ref get_unchecked_create /// the loop of @ref get_unchecked_create
template<typename BasicJsonType> template<typename BasicJsonType>
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) for (const auto* token = first; token != end; ++token)
{ {
const auto& reference_token = *token; const auto& reference_token = *token;
@@ -24831,16 +24925,25 @@ class json_pointer
return std::isdigit(x); return std::isdigit(x);
}); });
// change value to an array for array indices or "-" or to object otherwise // change value to an array for array indices or "-" or to an
*ptr = (nums || reference_token == "-") // object otherwise; every null value here is a fresh one (see
? detail::value_t::array // get_unchecked_rollback), so converting it in place leaves
: detail::value_t::object; // the same value as assigning an array or object to it
if (nums || is_dash(reference_token))
{
ptr->template convert_null_to<typename BasicJsonType::array_t>();
}
else
{
ptr->template convert_null_to<typename BasicJsonType::object_t>();
}
} }
if (ptr->is_object()) if (ptr->is_object())
{ {
// use unchecked object access // the key is missing: the object is new, or get_unchecked did
ptr = &object_member(*ptr, reference_token); // not find the key
ptr = &object_add(*ptr, reference_token);
} }
else else
{ {
@@ -24850,7 +24953,7 @@ class json_pointer
if (is_dash(reference_token)) if (is_dash(reference_token))
{ {
// explicitly treat "-" as index beyond the end // explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); ptr = &array_append(*ptr);
} }
else else
{ {