Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1aa393226 | ||
|
|
9b2d0c6445 | ||
|
|
d2e05c8b45 | ||
|
|
d1566fe9e8 | ||
|
|
3351a8795b |
No files matched your search
@@ -90,7 +90,8 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
1. Constant.
|
1. Constant.
|
||||||
2. Logarithmic in the size of the container.
|
2. Logarithmic in the size of the container.
|
||||||
3. Logarithmic in the size of the container.
|
3. Logarithmic in the size of the container.
|
||||||
4. Logarithmic in the size of the container.
|
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||||
|
constant for an array.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,20 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
|
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
|
||||||
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
|
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
|
||||||
|
|
||||||
|
If the **non-const** version throws, the value is left unchanged: values it created for earlier reference tokens
|
||||||
|
of `ptr` are removed again. This also holds for exceptions such as `#!cpp std::bad_alloc` or
|
||||||
|
`#!cpp std::length_error` thrown while padding an array up to a very large index. With custom
|
||||||
|
[template parameters](../../features/types/template_parameters.md), this requires that `erase()` of the object and
|
||||||
|
array types and the move operations of the custom base class do not throw.
|
||||||
|
|
||||||
## Complexity
|
## Complexity
|
||||||
|
|
||||||
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
|
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
|
||||||
2. Logarithmic in the size of the container.
|
2. Logarithmic in the size of the container.
|
||||||
3. Logarithmic in the size of the container.
|
3. Logarithmic in the size of the container.
|
||||||
4. Logarithmic in the size of the container.
|
4. Linear in the number of reference tokens of `ptr`; for each of them, logarithmic in the size of the object, or
|
||||||
|
constant for an array. When the non-const version pads an array with `#!json null` values, it is additionally
|
||||||
|
linear in `idx - size()` for that array.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -145,6 +153,14 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
|
specified by the JSON Pointer RFC; it is this library's own, intentional disambiguation rule. See also
|
||||||
[JSON Pointer](../../features/json_pointer.md).
|
[JSON Pointer](../../features/json_pointer.md).
|
||||||
|
|
||||||
|
!!! warning "Large array indices"
|
||||||
|
|
||||||
|
As the array is filled up with `#!json null` values to the given index, the memory used grows with the index
|
||||||
|
and not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB.
|
||||||
|
The library does not limit the index, so check the array indices of a JSON pointer from untrusted input
|
||||||
|
before using it with the non-const version. See
|
||||||
|
[Parsing untrusted input](../../features/parsing/untrusted_input.md#json-pointers-and-flattened-keys).
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
!!! warning "Deprecation"
|
||||||
|
|
||||||
Overload (4) also accepts a [`json_pointer`](../json_pointer/index.md) whose template argument is a `basic_json`
|
Overload (4) also accepts a [`json_pointer`](../json_pointer/index.md) whose template argument is a `basic_json`
|
||||||
@@ -294,4 +310,6 @@ Strong exception safety: if an exception occurs, the original value stays intact
|
|||||||
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
|
4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
|
||||||
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
version 3.13.0. Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
|
||||||
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
|
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not
|
||||||
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
|
a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. Fixed in version 3.13.0 to leave the value
|
||||||
|
unchanged if the non-const version throws (strong exception safety, as documented); before, values created for earlier
|
||||||
|
reference tokens of `ptr` were kept.
|
||||||
@@ -130,6 +130,17 @@ parameters of [`parse()`](../../api/basic_json/parse.md) or [`accept()`](../../a
|
|||||||
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
|
enable either for input whose conformance you cannot otherwise control, since interoperability with strictly
|
||||||
conforming JSON consumers is exactly what the default rejects.
|
conforming JSON consumers is exactly what the default rejects.
|
||||||
|
|
||||||
|
## JSON Pointers and flattened keys
|
||||||
|
|
||||||
|
A [JSON Pointer](../json_pointer.md) can come from untrusted input as well, for instance as a path in a request or as a
|
||||||
|
key of a flattened object. The non-const [`operator[]`](../../api/basic_json/operator%5B%5D.md) with a JSON Pointer and
|
||||||
|
[`unflatten`](../../api/basic_json/unflatten.md) create missing values, and when an array index lies beyond the end of
|
||||||
|
an array, they fill the array with `#!json null` values up to that index. The memory used therefore grows with the
|
||||||
|
index, not with the size of the pointer: on a 64-bit platform, the index `10000000` allocates about 160 MB, the index
|
||||||
|
`100000000` about 1.6 GB. The library does not limit the index. For pointers from untrusted input, check the array
|
||||||
|
indices (or the pointer as a whole) before using them, or use [`at`](../../api/basic_json/at.md) or
|
||||||
|
[`contains`](../../api/basic_json/contains.md), which never add values.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
|
- Wrap parsing in a `#!cpp try`/`#!cpp catch` block, or use `allow_exceptions=false`/`accept()` if your environment
|
||||||
@@ -145,6 +156,8 @@ conforming JSON consumers is exactly what the default rejects.
|
|||||||
`JSON_STRICT_NUL_HANDLING`.
|
`JSON_STRICT_NUL_HANDLING`.
|
||||||
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
|
- Decide whether duplicate object keys should be an error for your application, and add a callback if so.
|
||||||
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
|
- Do not enable `ignore_comments` or `ignore_trailing_commas` for input that must be strictly conforming JSON.
|
||||||
|
- Check the array indices of untrusted JSON Pointers and flattened keys before passing them to the non-const
|
||||||
|
`operator[]` or `unflatten()`, which fill arrays with `#!json null` values up to the index.
|
||||||
|
|
||||||
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
|
For the broader design rationale and how it is tested (fuzzing, sanitizers, static analysis), see the
|
||||||
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
|
[assurance case](../../community/assurance_case.md) and [quality assurance](../../community/quality_assurance.md). To
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm> // all_of, copy
|
#include <algorithm> // copy
|
||||||
#include <array> // array
|
#include <array> // array
|
||||||
#include <cctype> // isdigit
|
#include <cctype> // isdigit
|
||||||
#include <cerrno> // errno, ERANGE
|
#include <cerrno> // errno, ERANGE
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
#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
|
||||||
|
#include <iterator> // next
|
||||||
#include <limits> // max
|
#include <limits> // max
|
||||||
#include <string> // string
|
#include <string> // string
|
||||||
#include <utility> // move
|
#include <utility> // move
|
||||||
@@ -28,6 +30,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 +137,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,6 +612,228 @@ class json_pointer
|
|||||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief append a null value to @a array, like operator[] with the size of @a array
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
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<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>
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return members.lower_bound(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
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<typename ObjectType>
|
||||||
|
static auto object_search(ObjectType& members, const string_t& key)
|
||||||
|
-> decltype(object_search_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
|
||||||
|
|
||||||
|
@ref get_unchecked walks existing values without changing them. At the
|
||||||
|
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.
|
||||||
|
|
||||||
|
Reverting the change must not throw, as it happens in a destructor. It
|
||||||
|
erases the inserted member by its iterator (no key is converted or
|
||||||
|
compared, nothing is allocated), erases the added elements at the end of
|
||||||
|
the array, or swaps the null value back. This holds for std::map,
|
||||||
|
std::vector, ordered_map, and every object_t, array_t, and
|
||||||
|
json_base_class_t whose erase() and move operations do not throw.
|
||||||
|
Between recording and reverting, nothing is inserted into or erased from
|
||||||
|
the object or array, so the iterator stays valid.
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
class get_unchecked_rollback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using object_iterator = typename BasicJsonType::object_t::iterator;
|
||||||
|
|
||||||
|
/// record that @a member was inserted into the object @a value
|
||||||
|
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
|
||||||
|
: m_value(value)
|
||||||
|
, m_member(member)
|
||||||
|
, m_change(change::key_inserted)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// record the change that the next reference token makes to the null value or array @a value
|
||||||
|
explicit get_unchecked_rollback(BasicJsonType* value)
|
||||||
|
: m_value(value)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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 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_member);
|
||||||
|
break;
|
||||||
|
case change::array_grown:
|
||||||
|
{
|
||||||
|
auto& elements = *m_value->m_data.m_value.array;
|
||||||
|
elements.resize(m_old_size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case change::none:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class change
|
||||||
|
{
|
||||||
|
none,
|
||||||
|
null_converted,
|
||||||
|
key_inserted,
|
||||||
|
array_grown
|
||||||
|
};
|
||||||
|
|
||||||
|
BasicJsonType* m_value;
|
||||||
|
object_iterator m_member {};
|
||||||
|
typename BasicJsonType::size_type m_old_size = 0;
|
||||||
|
change m_change = change::none;
|
||||||
|
BasicJsonType m_saved_null {};
|
||||||
|
};
|
||||||
|
|
||||||
JSON_PRIVATE_UNLESS_TESTED:
|
JSON_PRIVATE_UNLESS_TESTED:
|
||||||
json_pointer top() const
|
json_pointer top() const
|
||||||
{
|
{
|
||||||
@@ -972,47 +1217,245 @@ class json_pointer
|
|||||||
|
|
||||||
Complexity: Linear in the length of the JSON pointer.
|
Complexity: Linear in the length of the JSON pointer.
|
||||||
|
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
Exception safety: Strong guarantee. This function walks existing values
|
||||||
@throw parse_error.109 if an array index was not a number
|
until the first reference token that does not select one. If that is the
|
||||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
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 of an existing array begins with '0'
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a number
|
||||||
|
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
|
||||||
|
array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
*/
|
*/
|
||||||
template<typename BasicJsonType>
|
template<typename BasicJsonType>
|
||||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||||
{
|
{
|
||||||
for (const auto& reference_token : reference_tokens)
|
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, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
// the key is inserted below or in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// explicitly treat "-" as index beyond the end
|
||||||
|
return array_append(*ptr);
|
||||||
|
}
|
||||||
|
// an element will be 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);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
// the array will be padded in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value will be converted in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (++token != end);
|
||||||
|
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (token + 1 == end && ptr->is_object())
|
||||||
|
{
|
||||||
|
// insert the missing key where the walk found it belongs
|
||||||
|
return object_insert(*ptr, it, *token);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
|
||||||
|
If the emplace() of the object type finds a member that the lookup in @ref
|
||||||
|
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
|
||||||
|
is inserted, and the walk continues with that member as in @ref
|
||||||
|
get_unchecked, which is what basic_json::operator[] does.
|
||||||
|
|
||||||
|
@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 of an existing array begins with
|
||||||
|
'0' (only after emplace() found a member, see above)
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a
|
||||||
|
number (only after emplace() found a member)
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull, or (only
|
||||||
|
after emplace() found a member) if the JSON pointer
|
||||||
|
can not be resolved
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
const auto* token = first;
|
||||||
|
while (ptr->is_object())
|
||||||
|
{
|
||||||
|
const auto old_capacity = ptr->container_capacity();
|
||||||
|
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
|
||||||
|
if (JSON_HEDLEY_LIKELY(inserted.second))
|
||||||
|
{
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
|
||||||
|
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing was inserted: walk existing values from the member
|
||||||
|
// emplace() found on, like get_unchecked
|
||||||
|
ptr = &inserted.first->second;
|
||||||
|
while (++token != end)
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
const auto it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr);
|
||||||
|
BasicJsonType& result = create_values(ptr, token, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief the loop of @ref get_unchecked_create
|
||||||
|
|
||||||
|
@a ptr is a null value or an array. Every value created here is a fresh
|
||||||
|
one, so every object a key is added to is empty.
|
||||||
|
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
for (const auto* token = first; token != end; ++token)
|
||||||
|
{
|
||||||
|
const auto& reference_token = *token;
|
||||||
|
|
||||||
// convert null values to arrays or objects before continuing
|
// convert null values to arrays or objects before continuing
|
||||||
if (ptr->is_null())
|
if (ptr->is_null())
|
||||||
{
|
{
|
||||||
// check if the reference token is a valid array index, that is
|
// change value to an array for array indices or "-" or to an
|
||||||
// a nonempty sequence of digits without a leading '0'
|
// object otherwise; every null value here is a fresh one (see
|
||||||
// (cf. RFC 6901, Sect. 4); tokens that could never be a valid
|
// get_unchecked_rollback), so converting it in place leaves
|
||||||
// array index (such as "01" or "") are treated as object keys
|
// the same value as assigning an array or object to it
|
||||||
const bool nums = !reference_token.empty()
|
if (is_array_index_token(reference_token) || is_dash(reference_token))
|
||||||
&& (reference_token.size() == 1 || reference_token[0] != '0')
|
|
||||||
&& std::all_of(reference_token.begin(), reference_token.end(),
|
|
||||||
[](const unsigned char x)
|
|
||||||
{
|
{
|
||||||
return std::isdigit(x);
|
ptr->template convert_null_to<typename BasicJsonType::array_t>();
|
||||||
});
|
}
|
||||||
|
else
|
||||||
// change value to an array for array indices or "-" or to object otherwise
|
{
|
||||||
*ptr = (nums || reference_token == "-")
|
ptr->template convert_null_to<typename BasicJsonType::object_t>();
|
||||||
? detail::value_t::array
|
}
|
||||||
: detail::value_t::object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ptr->is_object())
|
if (ptr->is_object())
|
||||||
{
|
{
|
||||||
// use unchecked object access
|
// the object is new and empty
|
||||||
ptr = &object_member(*ptr, reference_token);
|
ptr = &object_add(*ptr, reference_token);
|
||||||
}
|
}
|
||||||
else if (ptr->is_array())
|
else
|
||||||
{
|
{
|
||||||
if (reference_token == "-")
|
// only the first value is not created here, and it is a null
|
||||||
|
// value or an array (see get_unchecked_create)
|
||||||
|
JSON_ASSERT(ptr->is_array());
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -1020,16 +1463,31 @@ class json_pointer
|
|||||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// primitive value with a reference token left
|
|
||||||
throw_unresolved(reference_token, ptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return *ptr;
|
return *ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether @a s is a valid array index, that is a nonempty sequence of
|
||||||
|
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
|
||||||
|
/// never be a valid array index (such as "01" or "") are object keys
|
||||||
|
static bool is_array_index_token(const string_t& s)
|
||||||
|
{
|
||||||
|
const std::size_t size = s.size();
|
||||||
|
if (size == 0 || (size > 1 && s[0] == '0'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@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
|
||||||
|
|||||||
Generated
+489
-30
@@ -24064,7 +24064,7 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#include <algorithm> // all_of, copy
|
#include <algorithm> // copy
|
||||||
#include <array> // array
|
#include <array> // array
|
||||||
#include <cctype> // isdigit
|
#include <cctype> // isdigit
|
||||||
#include <cerrno> // errno, ERANGE
|
#include <cerrno> // errno, ERANGE
|
||||||
@@ -24072,9 +24072,11 @@ 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
|
||||||
|
#include <iterator> // next
|
||||||
#include <limits> // max
|
#include <limits> // max
|
||||||
#include <string> // string
|
#include <string> // string
|
||||||
#include <utility> // move
|
#include <utility> // move
|
||||||
@@ -24088,6 +24090,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>
|
||||||
@@ -24198,6 +24202,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
|
||||||
@@ -24653,6 +24677,228 @@ class json_pointer
|
|||||||
return object_member_impl(object, key, detail::priority_tag<1> {});
|
return object_member_impl(object, key, detail::priority_tag<1> {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief append a null value to @a array, like operator[] with the size of @a array
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
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<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>
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return members.lower_bound(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename ObjectType>
|
||||||
|
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<typename ObjectType>
|
||||||
|
static auto object_search(ObjectType& members, const string_t& key)
|
||||||
|
-> decltype(object_search_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
|
||||||
|
|
||||||
|
@ref get_unchecked walks existing values without changing them. At the
|
||||||
|
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.
|
||||||
|
|
||||||
|
Reverting the change must not throw, as it happens in a destructor. It
|
||||||
|
erases the inserted member by its iterator (no key is converted or
|
||||||
|
compared, nothing is allocated), erases the added elements at the end of
|
||||||
|
the array, or swaps the null value back. This holds for std::map,
|
||||||
|
std::vector, ordered_map, and every object_t, array_t, and
|
||||||
|
json_base_class_t whose erase() and move operations do not throw.
|
||||||
|
Between recording and reverting, nothing is inserted into or erased from
|
||||||
|
the object or array, so the iterator stays valid.
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
class get_unchecked_rollback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using object_iterator = typename BasicJsonType::object_t::iterator;
|
||||||
|
|
||||||
|
/// record that @a member was inserted into the object @a value
|
||||||
|
get_unchecked_rollback(BasicJsonType* value, const object_iterator& member)
|
||||||
|
: m_value(value)
|
||||||
|
, m_member(member)
|
||||||
|
, m_change(change::key_inserted)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// record the change that the next reference token makes to the null value or array @a value
|
||||||
|
explicit get_unchecked_rollback(BasicJsonType* value)
|
||||||
|
: m_value(value)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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 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_member);
|
||||||
|
break;
|
||||||
|
case change::array_grown:
|
||||||
|
{
|
||||||
|
auto& elements = *m_value->m_data.m_value.array;
|
||||||
|
elements.resize(m_old_size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case change::none:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class change
|
||||||
|
{
|
||||||
|
none,
|
||||||
|
null_converted,
|
||||||
|
key_inserted,
|
||||||
|
array_grown
|
||||||
|
};
|
||||||
|
|
||||||
|
BasicJsonType* m_value;
|
||||||
|
object_iterator m_member {};
|
||||||
|
typename BasicJsonType::size_type m_old_size = 0;
|
||||||
|
change m_change = change::none;
|
||||||
|
BasicJsonType m_saved_null {};
|
||||||
|
};
|
||||||
|
|
||||||
JSON_PRIVATE_UNLESS_TESTED:
|
JSON_PRIVATE_UNLESS_TESTED:
|
||||||
json_pointer top() const
|
json_pointer top() const
|
||||||
{
|
{
|
||||||
@@ -25036,47 +25282,245 @@ class json_pointer
|
|||||||
|
|
||||||
Complexity: Linear in the length of the JSON pointer.
|
Complexity: Linear in the length of the JSON pointer.
|
||||||
|
|
||||||
@throw parse_error.106 if an array index begins with '0'
|
Exception safety: Strong guarantee. This function walks existing values
|
||||||
@throw parse_error.109 if an array index was not a number
|
until the first reference token that does not select one. If that is the
|
||||||
@throw out_of_range.404 if the JSON pointer can not be resolved
|
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 of an existing array begins with '0'
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a number
|
||||||
|
@throw out_of_range.404 if the JSON pointer can not be resolved, or if an
|
||||||
|
array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
*/
|
*/
|
||||||
template<typename BasicJsonType>
|
template<typename BasicJsonType>
|
||||||
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
|
||||||
{
|
{
|
||||||
for (const auto& reference_token : reference_tokens)
|
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, token, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
// the key is inserted below or in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
if (token + 1 == end)
|
||||||
|
{
|
||||||
|
// explicitly treat "-" as index beyond the end
|
||||||
|
return array_append(*ptr);
|
||||||
|
}
|
||||||
|
// an element will be 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);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
// the array will be padded in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
// the null value will be converted in get_unchecked_create
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (++token != end);
|
||||||
|
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
if (token + 1 == end && ptr->is_object())
|
||||||
|
{
|
||||||
|
// insert the missing key where the walk found it belongs
|
||||||
|
return object_insert(*ptr, it, *token);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
|
||||||
|
If the emplace() of the object type finds a member that the lookup in @ref
|
||||||
|
get_unchecked did not find (the two disagree, e.g. for fifo_map), nothing
|
||||||
|
is inserted, and the walk continues with that member as in @ref
|
||||||
|
get_unchecked, which is what basic_json::operator[] does.
|
||||||
|
|
||||||
|
@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 of an existing array begins with
|
||||||
|
'0' (only after emplace() found a member, see above)
|
||||||
|
@throw parse_error.109 if an array index of an existing array was not a
|
||||||
|
number (only after emplace() found a member)
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull, or (only
|
||||||
|
after emplace() found a member) if the JSON pointer
|
||||||
|
can not be resolved
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
JSON_HEDLEY_NEVER_INLINE static BasicJsonType& get_unchecked_create(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
const auto* token = first;
|
||||||
|
while (ptr->is_object())
|
||||||
|
{
|
||||||
|
const auto old_capacity = ptr->container_capacity();
|
||||||
|
const auto inserted = ptr->m_data.m_value.object->emplace(*token, nullptr);
|
||||||
|
if (JSON_HEDLEY_LIKELY(inserted.second))
|
||||||
|
{
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr, inserted.first);
|
||||||
|
BasicJsonType& result = create_values(&ptr->set_parent_after_insert(inserted.first->second, old_capacity), token + 1, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing was inserted: walk existing values from the member
|
||||||
|
// emplace() found on, like get_unchecked
|
||||||
|
ptr = &inserted.first->second;
|
||||||
|
while (++token != end)
|
||||||
|
{
|
||||||
|
if (ptr->is_object())
|
||||||
|
{
|
||||||
|
auto& members = *ptr->m_data.m_value.object;
|
||||||
|
const auto it = object_search(members, *token);
|
||||||
|
if (!object_search_found(members, it, *token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &it->second;
|
||||||
|
}
|
||||||
|
else if (ptr->is_array())
|
||||||
|
{
|
||||||
|
if (is_dash(*token))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto idx = array_index<BasicJsonType>(*token);
|
||||||
|
if (idx >= ptr->m_data.m_value.array->size())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||||
|
}
|
||||||
|
else if (ptr->is_null())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// primitive value with a reference token left
|
||||||
|
throw_unresolved(*token, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token == end)
|
||||||
|
{
|
||||||
|
return *ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_unchecked_rollback<BasicJsonType> rollback(ptr);
|
||||||
|
BasicJsonType& result = create_values(ptr, token, end);
|
||||||
|
rollback.commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief the loop of @ref get_unchecked_create
|
||||||
|
|
||||||
|
@a ptr is a null value or an array. Every value created here is a fresh
|
||||||
|
one, so every object a key is added to is empty.
|
||||||
|
|
||||||
|
@throw out_of_range.404 if an array index is too large for strtoull
|
||||||
|
@throw out_of_range.410 if an array index exceeds size_type
|
||||||
|
@throw std::length_error if padding an array exceeds its maximal size
|
||||||
|
@throw std::bad_alloc if an allocation fails
|
||||||
|
*/
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
static BasicJsonType& create_values(BasicJsonType* ptr, const string_t* first, const string_t* end)
|
||||||
|
{
|
||||||
|
for (const auto* token = first; token != end; ++token)
|
||||||
|
{
|
||||||
|
const auto& reference_token = *token;
|
||||||
|
|
||||||
// convert null values to arrays or objects before continuing
|
// convert null values to arrays or objects before continuing
|
||||||
if (ptr->is_null())
|
if (ptr->is_null())
|
||||||
{
|
{
|
||||||
// check if the reference token is a valid array index, that is
|
// change value to an array for array indices or "-" or to an
|
||||||
// a nonempty sequence of digits without a leading '0'
|
// object otherwise; every null value here is a fresh one (see
|
||||||
// (cf. RFC 6901, Sect. 4); tokens that could never be a valid
|
// get_unchecked_rollback), so converting it in place leaves
|
||||||
// array index (such as "01" or "") are treated as object keys
|
// the same value as assigning an array or object to it
|
||||||
const bool nums = !reference_token.empty()
|
if (is_array_index_token(reference_token) || is_dash(reference_token))
|
||||||
&& (reference_token.size() == 1 || reference_token[0] != '0')
|
|
||||||
&& std::all_of(reference_token.begin(), reference_token.end(),
|
|
||||||
[](const unsigned char x)
|
|
||||||
{
|
{
|
||||||
return std::isdigit(x);
|
ptr->template convert_null_to<typename BasicJsonType::array_t>();
|
||||||
});
|
}
|
||||||
|
else
|
||||||
// change value to an array for array indices or "-" or to object otherwise
|
{
|
||||||
*ptr = (nums || reference_token == "-")
|
ptr->template convert_null_to<typename BasicJsonType::object_t>();
|
||||||
? detail::value_t::array
|
}
|
||||||
: detail::value_t::object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ptr->is_object())
|
if (ptr->is_object())
|
||||||
{
|
{
|
||||||
// use unchecked object access
|
// the object is new and empty
|
||||||
ptr = &object_member(*ptr, reference_token);
|
ptr = &object_add(*ptr, reference_token);
|
||||||
}
|
}
|
||||||
else if (ptr->is_array())
|
else
|
||||||
{
|
{
|
||||||
if (reference_token == "-")
|
// only the first value is not created here, and it is a null
|
||||||
|
// value or an array (see get_unchecked_create)
|
||||||
|
JSON_ASSERT(ptr->is_array());
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -25084,16 +25528,31 @@ class json_pointer
|
|||||||
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// primitive value with a reference token left
|
|
||||||
throw_unresolved(reference_token, ptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return *ptr;
|
return *ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether @a s is a valid array index, that is a nonempty sequence of
|
||||||
|
/// digits without a leading '0' (cf. RFC 6901, Sect. 4); tokens that could
|
||||||
|
/// never be a valid array index (such as "01" or "") are object keys
|
||||||
|
static bool is_array_index_token(const string_t& s)
|
||||||
|
{
|
||||||
|
const std::size_t size = s.size();
|
||||||
|
if (size == 0 || (size > 1 && s[0] == '0'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(s[i])) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@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
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
|
|||||||
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
||||||
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
||||||
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
||||||
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
|
|
||||||
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
|
|
||||||
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
|
|
||||||
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
|
|
||||||
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
||||||
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
||||||
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
||||||
@@ -43,7 +39,7 @@ where `json_view` stands on another architecture; they are not meant for publica
|
|||||||
|
|
||||||
## What is measured
|
## What is measured
|
||||||
|
|
||||||
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||||
JSON-RPC request (`rpc`):
|
JSON-RPC request (`rpc`):
|
||||||
|
|
||||||
| workload | what it does |
|
| workload | what it does |
|
||||||
@@ -52,7 +48,6 @@ JSON-RPC request (`rpc`):
|
|||||||
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
||||||
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
||||||
| dump | serialize a parsed document (compact) |
|
| dump | serialize a parsed document (compact) |
|
||||||
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
|
|
||||||
|
|
||||||
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
||||||
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
||||||
@@ -77,32 +72,11 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
|
|||||||
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
||||||
slower on citm_catalog traverse).
|
slower on citm_catalog traverse).
|
||||||
|
|
||||||
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
|
|
||||||
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
|
|
||||||
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
|
|
||||||
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
|
|
||||||
|
|
||||||
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
|
|
||||||
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
|
|
||||||
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
|
|
||||||
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
|
|
||||||
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
|
|
||||||
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
|
|
||||||
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
|
|
||||||
dropped, so lazy conversion is the only mode.
|
|
||||||
|
|
||||||
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
|
|
||||||
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
|
|
||||||
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
|
|
||||||
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
|
|
||||||
editable `json_view` unless it says so.
|
|
||||||
|
|
||||||
The engines do not all offer the same features, which the numbers should be read with:
|
The engines do not all offer the same features, which the numbers should be read with:
|
||||||
|
|
||||||
| engine | document | random access | editable | notes |
|
| engine | document | random access | editable | notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
||||||
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
|
|
||||||
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
||||||
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
||||||
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
|
|
||||||
// Same-feature-set benchmark: read-only JSON documents with random access.
|
// Same-feature-set benchmark: read-only JSON documents with random access.
|
||||||
//
|
//
|
||||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
|
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
|
||||||
// "(editable)": json_editable_document, same read API)
|
// yyjson yyjson_read(): immutable document, random access
|
||||||
// yyjson yyjson_read(): immutable document, random access
|
|
||||||
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
||||||
// per parse): immutable, random access
|
// per parse): immutable, random access
|
||||||
// references (different feature sets):
|
// references (different feature sets):
|
||||||
@@ -20,11 +19,7 @@
|
|||||||
//
|
//
|
||||||
// Workloads: parse (build + free), traverse (visit everything, convert every
|
// Workloads: parse (build + free), traverse (visit everything, convert every
|
||||||
// number, touch every string and key), select (a few fields per document),
|
// number, touch every string and key), select (a few fields per document),
|
||||||
// dump (compact serialization of the parsed document), and parse+dump (a fresh
|
// dump (compact serialization of the parsed document).
|
||||||
// document, then its dump, in one timed call). json_view converts floating-point
|
|
||||||
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
|
|
||||||
// for float-heavy documents, traverse (which includes the parse) and parse+dump
|
|
||||||
// are the fair reading of the parse and dump rows.
|
|
||||||
// All engines run interleaved in every round, each timed call after an untimed
|
// All engines run interleaved in every round, each timed call after an untimed
|
||||||
// one of the same engine; the best round is reported.
|
// one of the same engine; the best round is reported.
|
||||||
#include <nlohmann/json_view.hpp>
|
#include <nlohmann/json_view.hpp>
|
||||||
@@ -48,8 +43,6 @@
|
|||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
using nlohmann::json_document;
|
using nlohmann::json_document;
|
||||||
using nlohmann::json_editable_document;
|
|
||||||
using nlohmann::json_editable_view;
|
|
||||||
using nlohmann::json_view;
|
using nlohmann::json_view;
|
||||||
|
|
||||||
static volatile double g_sink;
|
static volatile double g_sink;
|
||||||
@@ -62,8 +55,7 @@ struct stats
|
|||||||
|
|
||||||
// ---------------- traversal ----------------
|
// ---------------- traversal ----------------
|
||||||
|
|
||||||
template<class V>
|
static void walk(json_view v, stats& st)
|
||||||
static void walk_view(V v, stats& st)
|
|
||||||
{
|
{
|
||||||
++st.nodes;
|
++st.nodes;
|
||||||
switch (v.type())
|
switch (v.type())
|
||||||
@@ -72,42 +64,32 @@ static void walk_view(V v, stats& st)
|
|||||||
for (auto it = v.begin(); it != v.end(); ++it)
|
for (auto it = v.begin(); it != v.end(); ++it)
|
||||||
{
|
{
|
||||||
st.str += it.key().size();
|
st.str += it.key().size();
|
||||||
walk_view<V>(*it, st);
|
walk(*it, st);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case json::value_t::array:
|
case json::value_t::array:
|
||||||
for (const V e : v)
|
for (const json_view e : v)
|
||||||
{
|
{
|
||||||
walk_view<V>(e, st);
|
walk(e, st);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case json::value_t::string:
|
case json::value_t::string:
|
||||||
st.str += v.get_string().size();
|
st.str += v.get_string().size();
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_integer:
|
case json::value_t::number_integer:
|
||||||
st.num += static_cast<double>(v.template get<std::int64_t>());
|
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_unsigned:
|
case json::value_t::number_unsigned:
|
||||||
st.num += static_cast<double>(v.template get<std::uint64_t>());
|
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_float:
|
case json::value_t::number_float:
|
||||||
st.num += v.template get<double>();
|
st.num += v.get<double>();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void walk(json_view v, stats& st)
|
|
||||||
{
|
|
||||||
walk_view<json_view>(v, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void walk(json_editable_view v, stats& st)
|
|
||||||
{
|
|
||||||
walk_view<json_editable_view>(v, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void walk(const json& j, stats& st)
|
static void walk(const json& j, stats& st)
|
||||||
{
|
{
|
||||||
++st.nodes;
|
++st.nodes;
|
||||||
@@ -318,21 +300,20 @@ static void walk(const boost::json::value& v, stats& st)
|
|||||||
// status (one tweet): id, user.screen_name, retweet_count
|
// status (one tweet): id, user.screen_name, retweet_count
|
||||||
// rpc: method, params.minuend, id
|
// rpc: method, params.minuend, id
|
||||||
|
|
||||||
template<class V>
|
static double pick(const std::string& name, json_view r)
|
||||||
static double pick_view(const std::string& name, V r)
|
|
||||||
{
|
{
|
||||||
double acc = 0;
|
double acc = 0;
|
||||||
if (name == "twitter" || name == "status")
|
if (name == "twitter" || name == "status")
|
||||||
{
|
{
|
||||||
auto one = [&](V s)
|
auto one = [&](json_view s)
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
|
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||||
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
|
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||||
};
|
};
|
||||||
if (name == "twitter")
|
if (name == "twitter")
|
||||||
{
|
{
|
||||||
for (const V s : r["statuses"])
|
for (const json_view s : r["statuses"])
|
||||||
{
|
{
|
||||||
one(s);
|
one(s);
|
||||||
}
|
}
|
||||||
@@ -344,20 +325,20 @@ static double pick_view(const std::string& name, V r)
|
|||||||
}
|
}
|
||||||
else if (name == "citm_catalog")
|
else if (name == "citm_catalog")
|
||||||
{
|
{
|
||||||
for (const V p : r["performances"])
|
for (const json_view p : r["performances"])
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
|
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||||
}
|
}
|
||||||
acc += static_cast<double>(r["events"].size());
|
acc += static_cast<double>(r["events"].size());
|
||||||
}
|
}
|
||||||
else if (name == "canada")
|
else if (name == "canada")
|
||||||
{
|
{
|
||||||
const V g = r["features"][0]["geometry"];
|
const json_view g = r["features"][0]["geometry"];
|
||||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||||
}
|
}
|
||||||
else if (name == "jeopardy")
|
else if (name == "jeopardy")
|
||||||
{
|
{
|
||||||
for (const V q : r)
|
for (const json_view q : r)
|
||||||
{
|
{
|
||||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||||
acc += static_cast<double>(q["category"].get_string().size());
|
acc += static_cast<double>(q["category"].get_string().size());
|
||||||
@@ -366,21 +347,11 @@ static double pick_view(const std::string& name, V r)
|
|||||||
else if (name == "rpc")
|
else if (name == "rpc")
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(r["method"].get_string().size());
|
acc += static_cast<double>(r["method"].get_string().size());
|
||||||
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
|
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double pick(const std::string& name, json_view r)
|
|
||||||
{
|
|
||||||
return pick_view<json_view>(name, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double pick(const std::string& name, json_editable_view r)
|
|
||||||
{
|
|
||||||
return pick_view<json_editable_view>(name, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double pick(const std::string& name, const json& r)
|
static double pick(const std::string& name, const json& r)
|
||||||
{
|
{
|
||||||
double acc = 0;
|
double acc = 0;
|
||||||
@@ -663,35 +634,15 @@ int main(int argc, char** argv)
|
|||||||
walk(json::parse(dc.text), a);
|
walk(json::parse(dc.text), a);
|
||||||
auto d = json_document::parse(dc.text);
|
auto d = json_document::parse(dc.text);
|
||||||
walk(d.root(), b);
|
walk(d.root(), b);
|
||||||
auto ed = json_editable_document::parse(dc.text);
|
|
||||||
stats e;
|
|
||||||
walk(ed.root(), e);
|
|
||||||
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||||
walk(yyjson_doc_get_root(y), c);
|
walk(yyjson_doc_get_root(y), c);
|
||||||
simdjson::dom::parser p;
|
simdjson::dom::parser p;
|
||||||
walk(p.parse(dc.text).value(), dd);
|
walk(p.parse(dc.text).value(), dd);
|
||||||
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||||
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
||||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||||
yyjson_doc_free(y);
|
yyjson_doc_free(y);
|
||||||
|
|
||||||
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
|
|
||||||
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
|
|
||||||
const std::string dj = json::parse(dc.text).dump();
|
|
||||||
const std::string dv = d.root().dump();
|
|
||||||
const std::string de = ed.root().dump();
|
|
||||||
std::size_t yn = 0;
|
|
||||||
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
|
||||||
char* yo = yyjson_write(y2, 0, &yn);
|
|
||||||
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
|
|
||||||
std::free(yo);
|
|
||||||
yyjson_doc_free(y2);
|
|
||||||
stats sj_nodes, sv_nodes, se_nodes;
|
|
||||||
walk(json::parse(dj), sj_nodes);
|
|
||||||
walk(json::parse(dv), sv_nodes);
|
|
||||||
walk(json::parse(de), se_nodes);
|
|
||||||
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||||
@@ -718,7 +669,6 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"parse", {
|
workloads.push_back({"parse", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||||
@@ -730,7 +680,6 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"traverse", {
|
workloads.push_back({"traverse", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
||||||
@@ -742,31 +691,15 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"select", {
|
workloads.push_back({"select", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
||||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
||||||
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
||||||
}});
|
}});
|
||||||
// pipeline: a fresh document, then a dump of it, in one timed call
|
|
||||||
// (simdjson DOM reuses its parser, as in the parse row); the traverse
|
|
||||||
// workload above is already parse+traverse
|
|
||||||
workloads.push_back({"parse+dump", {
|
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
|
|
||||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
#if JSON_VIEW_BENCH_BOOST
|
|
||||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
#endif
|
|
||||||
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
}});
|
|
||||||
{
|
{
|
||||||
// serialization of an already parsed document
|
// serialization of an already parsed document
|
||||||
static json_document vd;
|
static json_document vd;
|
||||||
vd.read(s);
|
vd.read(s);
|
||||||
static json_editable_document ed = json_editable_document::parse(s);
|
|
||||||
ed = json_editable_document::parse(s);
|
|
||||||
static yyjson_doc* yd = nullptr;
|
static yyjson_doc* yd = nullptr;
|
||||||
if (yd)
|
if (yd)
|
||||||
{
|
{
|
||||||
@@ -780,7 +713,6 @@ int main(int argc, char** argv)
|
|||||||
jd = json::parse(s);
|
jd = json::parse(s);
|
||||||
workloads.push_back({"dump", {
|
workloads.push_back({"dump", {
|
||||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||||
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||||
@@ -808,7 +740,7 @@ int main(int argc, char** argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const double ref = best[0];
|
const double ref = best[0];
|
||||||
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
|
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||||
{
|
{
|
||||||
const double us = best[k] / 1e3;
|
const double us = best[k] / 1e3;
|
||||||
|
|||||||
@@ -239,8 +239,6 @@ def main():
|
|||||||
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
||||||
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
||||||
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
||||||
ap.add_argument('--aligned', action='store_true',
|
|
||||||
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
|
|
||||||
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
||||||
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
||||||
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
||||||
@@ -284,46 +282,32 @@ def main():
|
|||||||
+ ['-I' + d for d in lib.include])
|
+ ['-I' + d for d in lib.include])
|
||||||
objects.append(obj)
|
objects.append(obj)
|
||||||
|
|
||||||
cxx_version = output(cxx + ['--version'])
|
binaries = {}
|
||||||
is_clang = 'clang' in cxx_version.lower()
|
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||||
# alignment flags: functions and (gcc) loops on 64-byte boundaries
|
exe = os.path.join(args.build_dir, bench)
|
||||||
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
|
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||||
|
binaries[bench] = exe
|
||||||
# the builds: the default one and, with --aligned, one with aligned code
|
|
||||||
variants = [('default build', args.build_dir, [])]
|
|
||||||
if args.aligned:
|
|
||||||
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
|
|
||||||
|
|
||||||
|
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||||
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
||||||
# label -> {benchmark: output}
|
outputs = {}
|
||||||
all_outputs = {}
|
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
|
||||||
for label, build_dir, extra in variants:
|
capture_output=True, text=True).stdout
|
||||||
os.makedirs(build_dir, exist_ok=True)
|
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
|
||||||
binaries = {}
|
capture_output=True, text=True).stdout
|
||||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||||
exe = os.path.join(build_dir, bench)
|
capture_output=True, text=True).stdout
|
||||||
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||||
binaries[bench] = exe
|
capture_output=True, text=True).stdout
|
||||||
|
for name, text in outputs.items():
|
||||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
print(text)
|
||||||
outputs = {}
|
|
||||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
|
|
||||||
capture_output=True, text=True).stdout
|
|
||||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
|
|
||||||
capture_output=True, text=True).stdout
|
|
||||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
|
|
||||||
capture_output=True, text=True).stdout
|
|
||||||
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
|
|
||||||
cwd=build_dir, capture_output=True, text=True).stdout
|
|
||||||
for name, text in outputs.items():
|
|
||||||
print(text)
|
|
||||||
all_outputs[label] = outputs
|
|
||||||
|
|
||||||
# results with their metadata
|
# results with their metadata
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
||||||
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
||||||
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||||
|
cxx_version = output(cxx + ['--version'])
|
||||||
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
||||||
meta = [
|
meta = [
|
||||||
('date', f'{now:%Y-%m-%d %H:%M}'),
|
('date', f'{now:%Y-%m-%d %H:%M}'),
|
||||||
@@ -332,7 +316,6 @@ def main():
|
|||||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||||
('compiler', compiler_version),
|
('compiler', compiler_version),
|
||||||
('flags', ' '.join(flags)),
|
('flags', ' '.join(flags)),
|
||||||
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
|
|
||||||
('yyjson', libs['yyjson'].version),
|
('yyjson', libs['yyjson'].version),
|
||||||
('simdjson', libs['simdjson'].version),
|
('simdjson', libs['simdjson'].version),
|
||||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||||
@@ -345,20 +328,15 @@ def main():
|
|||||||
f.write('| | |\n|---|---|\n')
|
f.write('| | |\n|---|---|\n')
|
||||||
for key, value in meta:
|
for key, value in meta:
|
||||||
f.write(f'| {key} | {value} |\n')
|
f.write(f'| {key} | {value} |\n')
|
||||||
for label, outputs in all_outputs.items():
|
for name, text in outputs.items():
|
||||||
# (without --aligned there is only the default build, and no extra heading level)
|
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
|
||||||
prefix = f'{label}: ' if args.aligned else ''
|
|
||||||
for name, text in outputs.items():
|
|
||||||
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
|
|
||||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||||
for label, build_dir, _ in variants:
|
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
||||||
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
|
path = os.path.join(args.build_dir, name + '.csv')
|
||||||
path = os.path.join(build_dir, name + '.csv')
|
if os.path.isfile(path):
|
||||||
if os.path.isfile(path):
|
with open(path, encoding='utf-8') as f:
|
||||||
with open(path, encoding='utf-8') as f:
|
out.write(f'# {name}\n' + f.read())
|
||||||
# (the default build keeps its plain section names)
|
|
||||||
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
|
|
||||||
print(f'results: {stem}.md, {stem}.csv')
|
print(f'results: {stem}.md, {stem}.csv')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -373,6 +373,23 @@ TEST_CASE("alternative string type")
|
|||||||
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
|
CHECK_FALSE(j.contains(alt_json::json_pointer("/foo/bar")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("non-const operator[](json_pointer)")
|
||||||
|
{
|
||||||
|
// creating values must compile without string_t::begin()
|
||||||
|
alt_json j = alt_json::parse(R"({"a":{"b":[1]},"c":null})");
|
||||||
|
const alt_json before = j;
|
||||||
|
j[alt_json::json_pointer("/a/b/3/x/-/y")] = 1; // pad, append, create
|
||||||
|
j[alt_json::json_pointer("/a/z")] = 2; // insert a key
|
||||||
|
j[alt_json::json_pointer("/c/0/q")] = 3; // convert null
|
||||||
|
j[alt_json::json_pointer("/a/b/-")] = 4; // append
|
||||||
|
CHECK(j.dump() == R"({"a":{"b":[1,null,null,{"x":[{"y":1}]},4],"z":2},"c":[{"q":3}]})");
|
||||||
|
|
||||||
|
// a failing access leaves the value unchanged
|
||||||
|
j = before;
|
||||||
|
CHECK_THROWS_AS(j[alt_json::json_pointer("/a/n/99999999999999999999999")], alt_json::out_of_range&);
|
||||||
|
CHECK(j == before);
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("operator/(std::size_t)")
|
SECTION("operator/(std::size_t)")
|
||||||
{
|
{
|
||||||
// json_pointer::operator/=(std::size_t) must compile without string_t
|
// json_pointer::operator/=(std::size_t) must compile without string_t
|
||||||
|
|||||||
@@ -29,9 +29,64 @@ using nlohmann::json;
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// the non-const operator[] must throw for pointer and leave the value parsed
|
||||||
|
// from init unchanged (strong exception safety)
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee(const char* init, const std::string& pointer, const char* message)
|
||||||
|
{
|
||||||
|
CAPTURE(init)
|
||||||
|
CAPTURE(pointer)
|
||||||
|
Json j = Json::parse(init);
|
||||||
|
const Json before = Json::parse(init);
|
||||||
|
const typename Json::json_pointer ptr(pointer);
|
||||||
|
CHECK_THROWS_WITH_AS(j[ptr], message, typename Json::exception&);
|
||||||
|
CHECK(j == before);
|
||||||
|
}
|
||||||
|
|
||||||
|
// as above, for an index that fits into size_type, but not into an array
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee_length_error(const char* init, const std::string& pointer)
|
||||||
|
{
|
||||||
|
CAPTURE(init)
|
||||||
|
CAPTURE(pointer)
|
||||||
|
Json j = Json::parse(init);
|
||||||
|
const Json before = Json::parse(init);
|
||||||
|
const typename Json::json_pointer ptr(pointer);
|
||||||
|
CHECK_THROWS_AS(j[ptr], std::length_error&);
|
||||||
|
CHECK(j == before);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Json>
|
||||||
|
void check_strong_guarantee_cases()
|
||||||
|
{
|
||||||
|
const char* const unresolved = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999999'";
|
||||||
|
|
||||||
|
// the first change converts a null value
|
||||||
|
check_strong_guarantee<Json>("null", "/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>("null", "/a/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":null})", "/a/b/-/99999999999999999999999", unresolved);
|
||||||
|
// the first change inserts a key into an object
|
||||||
|
check_strong_guarantee<Json>("{}", "/a/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":{"b":1}})", "/a/c/d/99999999999999999999999", unresolved);
|
||||||
|
// the first change pads or appends to an array
|
||||||
|
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/5/99999999999999999999999", unresolved);
|
||||||
|
check_strong_guarantee<Json>(R"({"a":[1]})", "/a/-/x/1/99999999999999999999999", unresolved);
|
||||||
|
// an index that does not fit into size_type
|
||||||
|
check_strong_guarantee<Json>("null", "/x/18446744073709551615",
|
||||||
|
"[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type");
|
||||||
|
// an index that fits into size_type, but not into an array
|
||||||
|
const auto too_large = std::to_string((std::numeric_limits<typename Json::size_type>::max)() - 1);
|
||||||
|
check_strong_guarantee_length_error<Json>("null", "/" + too_large);
|
||||||
|
check_strong_guarantee_length_error<Json>(R"({"a":[[1]]})", "/a/0/3/" + too_large);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TEST_CASE("JSON pointers")
|
TEST_CASE("JSON pointers")
|
||||||
{
|
{
|
||||||
SECTION("errors")
|
SECTION("errors")
|
||||||
@@ -557,6 +612,23 @@ TEST_CASE("JSON pointers")
|
|||||||
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
|
CHECK_THROWS_WITH_AS(j.at("/01"_json_pointer),
|
||||||
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
"[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("a failing operator[] leaves the value unchanged")
|
||||||
|
{
|
||||||
|
check_strong_guarantee_cases<json>();
|
||||||
|
check_strong_guarantee_cases<nlohmann::ordered_json>();
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a successful operator[] still creates the same values")
|
||||||
|
{
|
||||||
|
json j = {{"a", {1}}};
|
||||||
|
j["/a/3/x/-/y"_json_pointer] = 1;
|
||||||
|
CHECK(j == json::parse(R"({"a":[1,null,null,{"x":[{"y":1}]}]})"));
|
||||||
|
j["/a/-"_json_pointer] = 2;
|
||||||
|
j["/a/0"_json_pointer] = 3;
|
||||||
|
j["/b"_json_pointer] = 4;
|
||||||
|
CHECK(j == json::parse(R"({"a":[3,null,null,{"x":[{"y":1}]},2],"b":4})"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("flatten")
|
SECTION("flatten")
|
||||||
|
|||||||
@@ -1499,6 +1499,23 @@ TEST_CASE("regression tests 1")
|
|||||||
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
|
CHECK(order.dump() == R"({"a":2,"m":{"y":1,"b":2},"new_key":3})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("operator[](json_pointer) when emplace() finds a key the lookup does not")
|
||||||
|
{
|
||||||
|
// a copied fifo_map restarts its timestamps, so a new key "k" gets
|
||||||
|
// the timestamp of "x": the lookup does not find "k", but emplace()
|
||||||
|
// finds "x" and inserts nothing; the pointer then continues with the
|
||||||
|
// member of "x", as basic_json::operator[] does
|
||||||
|
const my_json orig = my_json::parse(R"({"x":1})");
|
||||||
|
const std::array<const char*, 3> pointers = {{"/k/0", "/k/a/b", "/k/-"}};
|
||||||
|
for (const char* pointer : pointers)
|
||||||
|
{
|
||||||
|
CAPTURE(pointer)
|
||||||
|
my_json j = orig;
|
||||||
|
CHECK_THROWS_AS(j[my_json::json_pointer(pointer)], my_json::out_of_range&);
|
||||||
|
CHECK(j.dump() == R"({"x":1})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("issue #977 - Assigning between different json types")
|
SECTION("issue #977 - Assigning between different json types")
|
||||||
{
|
{
|
||||||
foo_json lj = ns::foo{3};
|
foo_json lj = ns::foo{3};
|
||||||
|
|||||||
Reference in new issue
Block a user