Compare commits

..
Author SHA1 Message Date
Niels Lohmann 673c470c66 Move binary reader error construction out of line
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 11:47:28 +02:00
Niels Lohmann 82250cbe02 Destroy the first levels of a value by recursion
The pointer-reversing walk in destroy_container() avoids the call stack but
moves a value around for every container it visits. Recurse for the first
recursion_depth_limit() levels instead - no allocation, no thread-local, a
bounded frame - and hand anything deeper to the walk. Destroy twitter
0.49 -> 0.36 ms, citm 0.80 -> 0.74 ms (3.12.0: 0.47 / 1.00).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 11:47:26 +02:00
Niels Lohmann 8b2d29f28b Speed up container destruction, iterator access and diff()
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-15 11:46:52 +02:00
8 changed files with 986 additions and 600 deletions

No files matched your search

+89 -33
View File
@@ -195,7 +195,7 @@ class binary_reader
if (JSON_HEDLEY_UNLIKELY(current != char_traits<char_type>::eof())) if (JSON_HEDLEY_UNLIKELY(current != char_traits<char_type>::eof()))
{ {
return last_byte_error(exception_id::unexpected_end_of_input, concat("expected end of input; last byte: 0x", get_token_string()), "value"); return last_byte_error(exception_id::unexpected_end_of_input, "expected end of input", "value", "");
} }
} }
@@ -317,7 +317,7 @@ class binary_reader
{ {
if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast<std::size_t>(document_size) != chars_read - document_start)) if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast<std::size_t>(document_size) != chars_read - document_start))
{ {
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document"); return bson_document_size_error(document_size, chars_read - document_start);
} }
return true; return true;
} }
@@ -509,7 +509,7 @@ class binary_reader
{ {
if (JSON_HEDLEY_UNLIKELY(len < 1)) if (JSON_HEDLEY_UNLIKELY(len < 1))
{ {
return last_byte_error(exception_id::unexpected_byte, concat("string length must be at least 1, is ", std::to_string(len)), "string"); return bson_length_error("string length must be at least 1, is ", len, "string");
} }
if (JSON_HEDLEY_UNLIKELY(!get_string(len - static_cast<NumberType>(1), result))) if (JSON_HEDLEY_UNLIKELY(!get_string(len - static_cast<NumberType>(1), result)))
@@ -539,7 +539,7 @@ class binary_reader
{ {
if (JSON_HEDLEY_UNLIKELY(len < 0)) if (JSON_HEDLEY_UNLIKELY(len < 0))
{ {
return last_byte_error(exception_id::unexpected_byte, concat("byte array length cannot be negative, is ", std::to_string(len)), "binary"); return bson_length_error("byte array length cannot be negative, is ", len, "binary");
} }
// All BSON binary values have a subtype // All BSON binary values have a subtype
@@ -629,9 +629,7 @@ class binary_reader
default: // anything else is not supported (yet) default: // anything else is not supported (yet)
{ {
const std::string cr_str = hex_byte(static_cast<std::uint8_t>(element_type)); return bson_unsupported_type_error(element_type_parse_position, element_type);
return sax->parse_error(element_type_parse_position, cr_str,
parse_error::create(exception_id::bson_unsupported_type, element_type_parse_position, concat("Unsupported BSON record type 0x", cr_str), nullptr));
} }
} }
} }
@@ -1060,6 +1058,7 @@ class binary_reader
@param[in] context parsing context for the error message @param[in] context parsing context for the error message
@return whether the SAX consumer accepts the parse error @return whether the SAX consumer accepts the parse error
*/ */
JSON_HEDLEY_NEVER_INLINE
bool cbor_indefinite_string_error(const char* type_name, const char* context) bool cbor_indefinite_string_error(const char* type_name, const char* context)
{ {
return last_byte_error(exception_id::invalid_string_or_size, concat("indefinite-length ", type_name, " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", get_token_string()), context); return last_byte_error(exception_id::invalid_string_or_size, concat("indefinite-length ", type_name, " is not allowed inside indefinite-length ", type_name, "; last byte: 0x", get_token_string()), context);
@@ -1139,7 +1138,7 @@ class binary_reader
default: default:
{ {
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0x60-0x7B)", inside_indefinite ? "" : " or indefinite string type (0x7F)", "; last byte: 0x", get_token_string()), "string"); return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x60-0x7B)", "string", inside_indefinite ? "" : " or indefinite string type (0x7F)");
} }
} }
} }
@@ -1269,7 +1268,7 @@ class binary_reader
break; break;
} }
return last_byte_error(exception_id::invalid_string_or_size, concat("only string keys are supported, but found ", found, "; last byte: 0x", get_token_string()), "object key"); return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found);
} }
/*! /*!
@@ -1350,7 +1349,7 @@ class binary_reader
default: default:
{ {
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0x40-0x5B)", inside_indefinite ? "" : " or indefinite binary array type (0x5F)", "; last byte: 0x", get_token_string()), "binary"); return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0x40-0x5B)", "binary", inside_indefinite ? "" : " or indefinite binary array type (0x5F)");
} }
} }
} }
@@ -1496,8 +1495,7 @@ class binary_reader
{ {
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(len) || len == detail::unknown_size())) if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(len) || len == detail::unknown_size()))
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, return container_size_error(context);
exception_message(concat("excessive ", context, " size"), "size"), nullptr));
} }
result = conditional_static_cast<std::size_t>(len); result = conditional_static_cast<std::size_t>(len);
return true; return true;
@@ -2065,7 +2063,7 @@ class binary_reader
default: default:
{ {
return last_byte_error(exception_id::invalid_string_or_size, concat("expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x", get_token_string()), "string"); return last_byte_error(exception_id::invalid_string_or_size, "expected length specification (0xA0-0xBF, 0xD9-0xDB)", "string", "");
} }
} }
} }
@@ -2157,7 +2155,7 @@ class binary_reader
break; break;
} }
return last_byte_error(exception_id::invalid_string_or_size, concat("only string keys are supported, but found ", found, "; last byte: 0x", get_token_string()), "object key"); return last_byte_error(exception_id::invalid_string_or_size, "only string keys are supported, but found ", "object key", found);
} }
/*! /*!
@@ -2655,8 +2653,7 @@ class binary_reader
} }
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(number))) if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::size_t>(number)))
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
exception_message("integer value overflow", "size"), nullptr));
} }
result = static_cast<std::size_t>(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char result = static_cast<std::size_t>(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char
return true; return true;
@@ -2753,8 +2750,7 @@ class binary_reader
} }
if (!value_in_range_of<std::size_t>(number)) if (!value_in_range_of<std::size_t>(number))
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size");
exception_message("integer value overflow", "size"), nullptr));
} }
result = detail::conditional_static_cast<std::size_t>(number); result = detail::conditional_static_cast<std::size_t>(number);
return true; return true;
@@ -2832,7 +2828,7 @@ class binary_reader
// or SIZE_MAX. // or SIZE_MAX.
if (JSON_HEDLEY_UNLIKELY(result > (std::numeric_limits<std::size_t>::max)() / i)) if (JSON_HEDLEY_UNLIKELY(result > (std::numeric_limits<std::size_t>::max)() / i))
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size");
} }
result *= i; result *= i;
// the pre-check above already rules out result becoming 0 // the pre-check above already rules out result becoming 0
@@ -2841,7 +2837,7 @@ class binary_reader
// unknown-size container (see get_ubjson_size_type()) // unknown-size container (see get_ubjson_size_type())
if (result == npos) if (result == npos)
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, exception_message("excessive ndarray size caused overflow", "size"), nullptr)); return out_of_range_error(exception_id::container_too_large, "excessive ndarray size caused overflow", "size");
} }
if (JSON_HEDLEY_UNLIKELY(!emit_unsigned(i))) if (JSON_HEDLEY_UNLIKELY(!emit_unsigned(i)))
{ {
@@ -2891,7 +2887,7 @@ class binary_reader
if (input_format == input_format_t::bjdata if (input_format == input_format_t::bjdata
&& JSON_HEDLEY_UNLIKELY(is_bjd_excluded_optimized_type(result.second))) && JSON_HEDLEY_UNLIKELY(is_bjd_excluded_optimized_type(result.second)))
{ {
return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type"); return optimized_type_error();
} }
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("type"))) if (JSON_HEDLEY_UNLIKELY(!unexpect_eof("type")))
@@ -2906,7 +2902,7 @@ class binary_reader
{ {
return false; return false;
} }
return last_byte_error(exception_id::unexpected_byte, concat("expected '#' after type information; last byte: 0x", get_token_string()), "size"); return last_byte_error(exception_id::unexpected_byte, "expected '#' after type information", "size", "");
} }
const bool is_error = get_ubjson_size_value(result.first, is_ndarray, 0, result.second); const bool is_error = get_ubjson_size_value(result.first, is_ndarray, 0, result.second);
@@ -3057,7 +3053,7 @@ class binary_reader
} }
if (JSON_HEDLEY_UNLIKELY(current > 127)) if (JSON_HEDLEY_UNLIKELY(current > 127))
{ {
return last_byte_error(exception_id::invalid_string_or_size, concat("byte after 'C' must be in range 0x00..0x7F; last byte: 0x", get_token_string()), "char"); return last_byte_error(exception_id::invalid_string_or_size, "byte after 'C' must be in range 0x00..0x7F", "char", "");
} }
string_t s(1, static_cast<typename string_t::value_type>(current)); string_t s(1, static_cast<typename string_t::value_type>(current));
return sax->string(s); return sax->string(s);
@@ -3141,8 +3137,7 @@ class binary_reader
if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F') if (JSON_HEDLEY_UNLIKELY((size_and_type.second == 'Z' || size_and_type.second == 'T' || size_and_type.second == 'F')
&& size_and_type.first > max_valueless_container_size)) && size_and_type.first > max_valueless_container_size))
{ {
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large, return out_of_range_error(exception_id::container_too_large, "excessive array size", "size");
exception_message("excessive array size", "size"), nullptr));
} }
if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second))) if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
@@ -3854,7 +3849,7 @@ class binary_reader
{ {
// in case of failure, advance position by 1 to report the failing location // in case of failure, advance position by 1 to report the failing location
++chars_read; ++chars_read;
sax->parse_error(chars_read, "<end of file>", parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr)); static_cast<void>(report_eof(context));
return false; return false;
} }
return true; return true;
@@ -4007,8 +4002,7 @@ class binary_reader
const auto result = static_cast<number_float_t>(number); const auto result = static_cast<number_float_t>(number);
if (JSON_HEDLEY_UNLIKELY(std::isfinite(number) && !std::isfinite(result))) if (JSON_HEDLEY_UNLIKELY(std::isfinite(number) && !std::isfinite(result)))
{ {
return sax->parse_error(chars_read, get_token_string(), return out_of_range_error(exception_id::number_overflow, "number overflow", "value");
out_of_range::create(exception_id::number_overflow, exception_message("number overflow", "value"), nullptr));
} }
return sax->number_float(result, ""); return sax->number_float(result, "");
} }
@@ -4223,22 +4217,82 @@ class binary_reader
{ {
if (JSON_HEDLEY_UNLIKELY(current == char_traits<char_type>::eof())) if (JSON_HEDLEY_UNLIKELY(current == char_traits<char_type>::eof()))
{ {
return sax->parse_error(chars_read, "<end of file>", return report_eof(context);
parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr));
} }
return true; return true;
} }
/*!
@brief reports an unexpected end of input (out of line to keep the exception
construction out of the hot reading functions)
@param[in] context further context information (for diagnostics)
@return the result of the SAX parser's parse_error()
*/
JSON_HEDLEY_NON_NULL(2)
JSON_HEDLEY_NEVER_INLINE
bool report_eof(const char* context) const
{
return sax->parse_error(chars_read, "<end of file>",
parse_error::create(exception_id::unexpected_end_of_input, chars_read, exception_message("unexpected end of input", context), nullptr));
}
/*!
@brief reports an out_of_range error at the last read byte (out of line)
*/
JSON_HEDLEY_NEVER_INLINE
bool out_of_range_error(const exception_id id_, const char* detail, const char* context) const
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(id_, exception_message(detail, context), nullptr));
}
JSON_HEDLEY_NEVER_INLINE
bool container_size_error(const char* what) const
{
return sax->parse_error(chars_read, get_token_string(), out_of_range::create(exception_id::container_too_large,
exception_message(concat("excessive ", what, " size"), "size"), nullptr));
}
JSON_HEDLEY_NEVER_INLINE
bool optimized_type_error() const
{
return last_byte_error(exception_id::unexpected_byte, concat("marker 0x", get_token_string(), " is not a permitted optimized array type"), "type");
}
template<typename NumberType>
JSON_HEDLEY_NEVER_INLINE
bool bson_length_error(const char* prefix, const NumberType len, const char* context) const
{
return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(len)), context);
}
JSON_HEDLEY_NEVER_INLINE
bool bson_document_size_error(const std::int32_t document_size, const std::size_t bytes_read) const
{
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document");
}
JSON_HEDLEY_NEVER_INLINE
bool bson_unsupported_type_error(const std::size_t position, const char element_type) const
{
const std::string cr_str = hex_byte(static_cast<std::uint8_t>(element_type));
return sax->parse_error(position, cr_str,
parse_error::create(exception_id::bson_unsupported_type, position, concat("Unsupported BSON record type 0x", cr_str), nullptr));
}
/*! /*!
@brief reports a parse error at the last read byte @brief reports a parse error at the last read byte
@param[in] id_ the id of the parse_error exception @param[in] id_ the id of the parse_error exception
@param[in] detail a detailed error message @param[in] detail a detailed error message
@param[in] context further context information @param[in] context further context information
@param[in] tail optional text appended to the detail, followed by "; last byte: 0x<token>"
@return the result of the SAX parser's parse_error() @return the result of the SAX parser's parse_error()
*/ */
bool last_byte_error(const exception_id id_, const std::string& detail, const std::string& context) const JSON_HEDLEY_NEVER_INLINE
bool last_byte_error(const exception_id id_, const std::string& detail, const char* context, const char* tail = nullptr) const
{ {
return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read, exception_message(detail, context), nullptr)); // with a tail, the message reads "<detail><tail>; last byte: 0x<token>"
return sax->parse_error(chars_read, get_token_string(), parse_error::create(id_, chars_read,
exception_message(tail == nullptr ? detail : concat(detail, tail, "; last byte: 0x", get_token_string()), context), nullptr));
} }
/*! /*!
@@ -4247,7 +4301,8 @@ class binary_reader
@param[in] context further context information @param[in] context further context information
@return the result of the SAX parser's parse_error() @return the result of the SAX parser's parse_error()
*/ */
bool unexpected_byte(const char* detail, const std::string& context) const JSON_HEDLEY_NEVER_INLINE
bool unexpected_byte(const char* detail, const char* context) const
{ {
return last_byte_error(exception_id::unexpected_byte, concat(detail, ": 0x", get_token_string()), context); return last_byte_error(exception_id::unexpected_byte, concat(detail, ": 0x", get_token_string()), context);
} }
@@ -4258,6 +4313,7 @@ class binary_reader
@param[in] context further context information @param[in] context further context information
@return the result of the SAX parser's parse_error() @return the result of the SAX parser's parse_error()
*/ */
JSON_HEDLEY_NEVER_INLINE
bool length_type_error(const char* position, const char* context) const bool length_type_error(const char* position, const char* context) const
{ {
const char* types = input_format == input_format_t::bjdata ? "U, i, u, I, m, l, M, L" : "U, i, I, l, L"; const char* types = input_format == input_format_t::bjdata ? "U, i, u, I, m, l, M, L" : "U, i, I, l, L";
+8 -1
View File
@@ -187,12 +187,19 @@ Shared by json_sax_dom_parser and json_sax_dom_callback_parser.
@param[in] ref the object or array that was just created @param[in] ref the object or array that was just created
@return whether parsing should continue (false after reporting out_of_range.408) @return whether parsing should continue (false after reporting out_of_range.408)
*/ */
template<typename SAX, typename BasicJsonType>
JSON_HEDLEY_NEVER_INLINE
bool container_too_large_error(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref)
{
return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref));
}
template<typename SAX, typename BasicJsonType> template<typename SAX, typename BasicJsonType>
bool check_container_size(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref) bool check_container_size(SAX& sax, std::size_t len, const char* kind, BasicJsonType* ref)
{ {
if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref->max_size())) if (JSON_HEDLEY_UNLIKELY(len != detail::unknown_size() && len > ref->max_size()))
{ {
return sax.parse_error(0, "", out_of_range::create(exception_id::container_too_large, concat("excessive ", kind, " size: ", std::to_string(len)), ref)); return container_too_large_error(sax, len, kind, ref);
} }
return true; return true;
} }
+66 -131
View File
@@ -294,40 +294,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type) if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{ {
case value_t::object: JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
{ return *m_it.array_iterator;
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return m_it.object_iterator->second;
}
case value_t::array:
{
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return *m_it.array_iterator;
}
case value_t::null:
throw_cannot_get_value();
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return *m_object;
}
throw_cannot_get_value();
}
} }
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return m_it.object_iterator->second;
}
// neither array nor object: a primitive value (null has no value to point to)
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type != value_t::null && m_it.primitive_iterator.is_begin()))
{
return *m_object;
}
throw_cannot_get_value();
} }
/*! /*!
@@ -338,38 +323,25 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type) if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{ {
case value_t::object: JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
{ return &*m_it.array_iterator;
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return &(m_it.object_iterator->second);
}
case value_t::array:
{
JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end());
return &*m_it.array_iterator;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return m_object;
}
throw_cannot_get_value();
}
} }
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end());
return &(m_it.object_iterator->second);
}
// neither array nor object: a primitive value
if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin()))
{
return m_object;
}
throw_cannot_get_value();
} }
/*! /*!
@@ -391,35 +363,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type) if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{ {
case value_t::object: ++m_it.array_iterator;
{ return *this;
std::advance(m_it.object_iterator, 1);
break;
}
case value_t::array:
{
std::advance(m_it.array_iterator, 1);
break;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
++m_it.primitive_iterator;
break;
}
} }
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
std::advance(m_it.object_iterator, 1);
return *this;
}
// neither array nor object: a primitive value
++m_it.primitive_iterator;
return *this; return *this;
} }
@@ -442,35 +399,20 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type) if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{ {
case value_t::object: --m_it.array_iterator;
{ return *this;
std::advance(m_it.object_iterator, -1);
break;
}
case value_t::array:
{
std::advance(m_it.array_iterator, -1);
break;
}
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
{
--m_it.primitive_iterator;
break;
}
} }
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
std::advance(m_it.object_iterator, -1);
return *this;
}
// neither array nor object: a primitive value
--m_it.primitive_iterator;
return *this; return *this;
} }
@@ -493,25 +435,18 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
return true; return true;
} }
switch (m_object->m_data.m_type) if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::array))
{ {
case value_t::object: return (m_it.array_iterator == other.m_it.array_iterator);
return (m_it.object_iterator == other.m_it.object_iterator);
case value_t::array:
return (m_it.array_iterator == other.m_it.array_iterator);
case value_t::null:
case value_t::string:
case value_t::boolean:
case value_t::number_integer:
case value_t::number_unsigned:
case value_t::number_float:
case value_t::binary:
case value_t::discarded:
default:
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
} }
if (JSON_HEDLEY_LIKELY(m_object->m_data.m_type == value_t::object))
{
return (m_it.object_iterator == other.m_it.object_iterator);
}
// neither array nor object: a primitive value
return (m_it.primitive_iterator == other.m_it.primitive_iterator);
} }
/*! /*!
+221 -125
View File
@@ -734,6 +734,34 @@ private:
} }
} }
// true if no child of the non-empty array/object v has children of
// its own, i.e. v can be freed in one go (its destructor then
// destroys scalars and empty containers only, so it recurses at most
// one level deep); stops at the first structured child it meets
static bool all_children_leaves(const basic_json& v) noexcept
{
if (v.m_data.m_type == value_t::array)
{
const auto& a = *v.m_data.m_value.array;
for (auto it = a.rbegin(); it != a.rend(); ++it)
{
if (!has_no_children(*it))
{
return false;
}
}
return true;
}
for (const auto& el : *v.m_data.m_value.object)
{
if (!has_no_children(el.second))
{
return false;
}
}
return true;
}
// The walk in destroy_container() may take the children of a // The walk in destroy_container() may take the children of a
// container in any order, as long as it picks the same child again // container in any order, as long as it picks the same child again
// while that container is not modified in between. Arrays and // while that container is not modified in between. Arrays and
@@ -789,15 +817,16 @@ private:
} }
} }
// deallocates the (already empty) array/object held by v; this is // deallocates the array/object held by v, which is either empty or
// the same allocator-based free the old recursive implementation // holds only scalars and empty containers (so its destructor
// used, just factored out so every level of the walk in destroy() // recurses at most one level deep); this is the same allocator-based
// can share it // free the old recursive implementation used, just factored out so
// every level of the walk in destroy() can share it
static void free_container(basic_json& v) noexcept static void free_container(basic_json& v) noexcept
{ {
JSON_ASSERT(has_no_children(v) || all_children_leaves(v));
if (v.m_data.m_type == value_t::array) if (v.m_data.m_type == value_t::array)
{ {
JSON_ASSERT(v.m_data.m_value.array->empty());
AllocatorType<array_t> alloc; AllocatorType<array_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.array);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.array, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.array, 1);
@@ -805,7 +834,6 @@ private:
else else
{ {
JSON_ASSERT(v.m_data.m_type == value_t::object); JSON_ASSERT(v.m_data.m_type == value_t::object);
JSON_ASSERT(v.m_data.m_value.object->empty());
AllocatorType<object_t> alloc; AllocatorType<object_t> alloc;
std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.object); std::allocator_traits<decltype(alloc)>::destroy(alloc, v.m_data.m_value.object);
std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.object, 1); std::allocator_traits<decltype(alloc)>::deallocate(alloc, v.m_data.m_value.object, 1);
@@ -850,7 +878,58 @@ public:
return; return;
} }
// Destroy the tree without recursing per nesting level and // This json_value is not itself a basic_json, so the top-level
// container is first moved into a local stand-in ("cur"); a
// default-constructed basic_json has a null pointer in its
// m_value (see data::m_value's initializer), so swapping it
// with *this leaves this union's own pointer null, and it is
// never looked at or freed a second time.
basic_json cur;
cur.m_data.m_type = t;
using std::swap;
swap(cur.m_data.m_value, *this);
destroy_bounded(cur, 0);
}
// Destroy the array/object v, whose children are destroyed first.
// The first recursion_depth_limit() levels are destroyed by plain
// recursion, which is the fastest way and cannot throw; anything
// nested deeper is handed to destroy_walk(), which uses no call stack.
static void destroy_bounded(basic_json& v, std::size_t depth) noexcept
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{
destroy_walk(v);
return;
}
if (v.m_data.m_type == value_t::array)
{
for (auto& child : *v.m_data.m_value.array)
{
if (child.m_data.m_type == value_t::array || child.m_data.m_type == value_t::object)
{
destroy_bounded(child, depth + 1);
}
}
}
else
{
for (auto& child : *v.m_data.m_value.object)
{
if (child.second.m_data.m_type == value_t::array || child.second.m_data.m_type == value_t::object)
{
destroy_bounded(child.second, depth + 1);
}
}
}
// only leaves and already destroyed (null) children remain
free_container(v);
}
// Destroy the tree below top without recursing per nesting level and
// without any heap allocation: a heap-allocated flattening // without any heap allocation: a heap-allocated flattening
// stack (the previous implementation) can itself throw // stack (the previous implementation) can itself throw
// bad_alloc, which would escape this noexcept destructor and // bad_alloc, which would escape this noexcept destructor and
@@ -874,16 +953,24 @@ public:
// pointer in its m_value (see data::m_value's initializer), // pointer in its m_value (see data::m_value's initializer),
// so swapping it with *this leaves this union's own pointer // so swapping it with *this leaves this union's own pointer
// null, and it is never looked at or freed a second time. // null, and it is never looked at or freed a second time.
static void destroy_walk(basic_json& top) noexcept
{
basic_json cur; basic_json cur;
cur.m_data.m_type = t; take(cur, top);
using std::swap;
swap(cur.m_data.m_value, *this);
basic_json prev; // value_t::null: no parent basic_json prev; // value_t::null: no parent
// true right after cur was entered: its children have not been
// looked at yet. If none of them has children of its own, cur is
// freed in one go instead of child by child; this is tried once
// per container, so the walk stays linear.
bool fresh = true;
while (true) while (true)
{ {
if (has_no_children(cur)) const bool fresh_now = fresh;
fresh = false;
if (has_no_children(cur) || (fresh_now && all_children_leaves(cur)))
{ {
if (prev.m_data.m_type == value_t::null) if (prev.m_data.m_type == value_t::null)
{ {
@@ -922,6 +1009,7 @@ public:
take(cur_child_ref, prev); take(cur_child_ref, prev);
take(prev, cur); take(prev, cur);
take(cur, tmp); take(cur, tmp);
fresh = true;
} }
} }
@@ -7137,12 +7225,11 @@ public:
std::size_t index = 0; std::size_t index = 0;
/// objects: the next member of source to look at /// objects: the next member of source to look at
const_iterator member{}; // NOLINT(readability-redundant-member-init) const_iterator member{}; // NOLINT(readability-redundant-member-init)
/// objects: the keys common to both, in source's order /// objects: for each member of source (in order), its value in
std::vector<typename object_t::key_type> common_keys{}; // NOLINT(readability-redundant-member-init) /// target, or nullptr if target has no such key; `index` is the next
/// objects: the next entry of common_keys std::vector<const basic_json*> target_values{}; // NOLINT(readability-redundant-member-init)
std::size_t next_common = 0; /// objects: the members only target has, in target's order
/// objects: the "add" operations for keys only target has std::vector<const_iterator> added{}; // NOLINT(readability-redundant-member-init)
basic_json added_ops{}; // NOLINT(readability-redundant-member-init)
}; };
// The operations of a diff are built by the functions below rather than // The operations of a diff are built by the functions below rather than
@@ -7198,111 +7285,113 @@ public:
} }
} }
/// @brief appends the "add" operations for the members @a added to @a result
static void diff_added(basic_json& result, const std::vector<const_iterator>& added, const string_t& path)
{
for (const auto& it : added)
{
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
}
}
/*! /*!
@brief compare the keys of objects @a source and @a target @brief compare the keys of objects @a source and @a target
If object_t does not keep its members in insertion order, or if the keys If object_t does not keep its members in insertion order, or if the keys
both objects have are in the same order in both, and the keys only both objects have are in the same order in both, and the keys only
@a target has come after them, stores the keys common to both in @a target has come after them, stores in @a target_values, for each member
source's order in @a common_keys, stores the "add" operations for the keys of source in order, its value in target (nullptr for a key target does not
only @a target has in @a added_ops, and returns true: the caller then diffs have), stores the members only target has in @a added, and returns true:
the objects member by member. Otherwise, appends operations that remove the caller then diffs the objects member by member. Otherwise, appends
every member of @a source and add every member of @a target to @a result, operations that remove every member of @a source and add every member of
and returns false. @a target to @a result, and returns false.
*/ */
static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target, static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, std::vector<typename object_t::key_type>& common_keys, const string_t& path, std::vector<const basic_json*>& target_values,
basic_json& added_ops) std::vector<const_iterator>& added)
{ {
// first pass: record, for every source key, whether it is // first pass: one lookup per source key
// common to both objects (in source's iteration order) or target_values.reserve(source.size());
// was deleted (i.e., in source but not in target) -- this is std::size_t common = 0;
// a by-product of the target.find() call already needed to
// tell the two cases apart, so it adds no extra lookups. The
// "remove" ops themselves are emitted later, interleaved
// with the per-key diffs in the caller's fast path, to match
// source's original iteration order (as the original,
// pre-reordering-aware implementation did) instead of
// grouping all removes before all per-key diffs.
std::vector<typename object_t::key_type> common_keys_source_order;
for (auto it = source.cbegin(); it != source.cend(); ++it) for (auto it = source.cbegin(); it != source.cend(); ++it)
{ {
if (target.find(it.key()) != target.end()) const auto found = target.find(it.key());
if (found != target.cend())
{ {
common_keys_source_order.push_back(it.key()); target_values.push_back(&found.value());
} ++common;
}
// second pass: find keys that were added (i.e., in target but
// not in source), and record the keys common to both, in
// target's iteration order -- again a by-product of the
// source.find() call already needed to detect added keys. At
// the same time, determine whether every added key comes
// after every common key in target's order (a precondition
// for the fast path, which only ever appends new keys
// at the very end). Both are only needed for an object_t that
// keeps its members in insertion order, such as the one
// backing `ordered_json`; for any other object_t, the fast
// path is always taken and they are not computed.
// The patch ops for keys that were added (i.e., in target but not
// in source) are built here so the fast path can reuse
// them without a second source.find() per target key. Only
// used by the fast path -- the slow (reordering) path
// rebuilds "add" ops for every key itself.
std::vector<typename object_t::key_type> common_keys_target_order;
bool new_keys_form_suffix = true;
bool seen_new_key = false;
for (auto it = target.cbegin(); it != target.cend(); ++it)
{
if (source.find(it.key()) == source.end())
{
seen_new_key = true;
diff_add(added_ops, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
} }
else else
{ {
#ifdef JSON_HEDLEY_MSVC_VERSION target_values.push_back(nullptr);
#pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif
if (detail::is_ordered_map<object_t>::value)
{
common_keys_target_order.push_back(it.key());
if (seen_new_key)
{
new_keys_form_suffix = false;
}
}
#ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning( pop )
#endif
} }
} }
// Only an object type that keeps its members in insertion // second pass: find the keys only target has. An object type that
// order, such as nlohmann::ordered_map, can need reordering: // keeps its members in insertion order, such as the one backing
// patch() appends a new member at the end of such an object. // ordered_json, also needs the common keys to be in the same order
// Any other object type places its members itself - std::map // in both objects, and the new keys to come after them, as patch()
// in key order, a hash map in an order its operator== ignores - // appends a new member at the end of such an object. Any other
// so a member-by-member diff always reproduces target there. // object type places its members itself, so a member-by-member diff
// always reproduces target there.
#ifdef JSON_HEDLEY_MSVC_VERSION #ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning(push ) #pragma warning(push )
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr #pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
#endif #endif
if (!detail::is_ordered_map<object_t>::value bool fast_path = true;
|| (common_keys_source_order == common_keys_target_order && new_keys_form_suffix)) if (detail::is_ordered_map<object_t>::value)
{ {
// fast path: order of common keys already matches (or the // walk the common keys of source in lockstep with target: a key
// object_t's iteration order does not depend on // of target that is not the next one is either new or out of order
// insertion history), so a plain per-key diff is correct auto src = source.cbegin();
// and minimal, as before std::size_t next = 0;
common_keys = std::move(common_keys_source_order); bool seen_new = false;
return true; for (auto it = target.cbegin(); it != target.cend() && fast_path; ++it)
{
while (next < target_values.size() && target_values[next] == nullptr)
{
++src;
++next;
}
if (next < target_values.size() && src.key() == it.key())
{
// a common key after a new one: new keys are no suffix
fast_path = !seen_new;
++src;
++next;
}
else if (source.find(it.key()) == source.cend())
{
seen_new = true;
added.push_back(it);
}
else
{
// a common key out of order
fast_path = false;
}
}
}
else if (target.size() != common)
{
for (auto it = target.cbegin(); it != target.cend(); ++it)
{
if (source.find(it.key()) == source.cend())
{
added.push_back(it);
}
}
} }
#ifdef JSON_HEDLEY_MSVC_VERSION #ifdef JSON_HEDLEY_MSVC_VERSION
#pragma warning( pop ) #pragma warning( pop )
#endif #endif
if (fast_path)
{
return true;
}
// slow path: the common keys are in a different relative // slow path: the common keys are in a different relative
// order in source and target (only possible for a // order in source and target (only possible for a
// reorderable object_t like ordered_map). Building a // reorderable object_t like ordered_map). Building a
@@ -7352,6 +7441,14 @@ public:
return; return;
} }
diff_unequal(result, source, target, path, depth);
}
/// @brief @ref diff_recursively for values known to be unequal; callers
/// compare first so they build no path for equal ones
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
const string_t& path, const std::size_t depth)
{
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
{ {
diff_iteratively(result, source, target, path); diff_iteratively(result, source, target, path);
@@ -7374,7 +7471,10 @@ public:
while (i < source.size() && i < target.size()) while (i < source.size() && i < target.size())
{ {
// recursive call to compare array values at index i // recursive call to compare array values at index i
diff_recursively(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1); if (!(source[i] == target[i]))
{
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
}
++i; ++i;
} }
@@ -7386,25 +7486,24 @@ public:
case value_t::object: case value_t::object:
{ {
std::vector<typename object_t::key_type> common_keys; std::vector<const basic_json*> target_values;
basic_json added_ops(value_t::array); std::vector<const_iterator> added;
if (diff_object_keys(result, source, target, path, common_keys, added_ops)) if (diff_object_keys(result, source, target, path, target_values, added))
{ {
// fast path: common_keys is, by construction, the // fast path: target_values holds, for every member of
// subsequence of source's keys that are common to both // source in order, its value in target -- so no further
// objects, in source's iteration order -- so it can be // lookup is needed. Deleted keys are interleaved here
// walked in lockstep with `source` using a cheap key // too, in source's original order, to match the
// comparison instead of another lookup. Deleted keys
// (those source keys not in common_keys) are interleaved
// here too, in source's original order, to match the
// historical (pre-reordering-aware) output order. // historical (pre-reordering-aware) output order.
auto common_it = common_keys.cbegin(); std::size_t i = 0;
for (auto it = source.cbegin(); it != source.cend(); ++it) for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
{ {
if (common_it != common_keys.cend() && it.key() == *common_it) if (target_values[i] != nullptr)
{ {
diff_recursively(result, it.value(), target[it.key()], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1); if (!(it.value() == *target_values[i]))
++common_it; {
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
}
} }
else else
{ {
@@ -7413,10 +7512,8 @@ public:
} }
} }
// append the "add" ops for brand-new keys collected by // append the "add" operations for brand-new keys
// diff_object_keys -- no second source.find() per target diff_added(result, added, path);
// key needed
result.insert(result.end(), added_ops.begin(), added_ops.end());
} }
break; break;
} }
@@ -7489,17 +7586,17 @@ public:
case value_t::object: case value_t::object:
{ {
std::vector<typename object_t::key_type> common_keys; std::vector<const basic_json*> target_values;
basic_json added_ops(value_t::array); std::vector<const_iterator> added;
if (diff_object_keys(result, s, t, current_path, common_keys, added_ops)) if (diff_object_keys(result, s, t, current_path, target_values, added))
{ {
// fast path: the frame walks source in lockstep with // fast path: the frame walks source in lockstep with
// common_keys, as diff_recursively does, and appends // common_keys, as diff_recursively does, and appends
// added_ops once all members are done // added_ops once all members are done
stack.emplace_back(&s, &t, current_path.size()); stack.emplace_back(&s, &t, current_path.size());
stack.back().member = s.cbegin(); stack.back().member = s.cbegin();
stack.back().common_keys = std::move(common_keys); stack.back().target_values = std::move(target_values);
stack.back().added_ops = std::move(added_ops); stack.back().added = std::move(added);
} }
return; return;
} }
@@ -7561,11 +7658,10 @@ public:
if (it != s->cend()) if (it != s->cend())
{ {
++stack.back().member; ++stack.back().member;
const std::size_t next_common = stack.back().next_common; const basic_json* const target_value_ptr = stack.back().target_values[stack.back().index++];
if (next_common < stack.back().common_keys.size() && it.key() == stack.back().common_keys[next_common]) if (target_value_ptr != nullptr)
{ {
++stack.back().next_common; const basic_json& target_value = *target_value_ptr;
const basic_json& target_value = (*t)[it.key()];
detail::concat_into(current_path, '/', detail::escape(it.key())); detail::concat_into(current_path, '/', detail::escape(it.key()));
enter(it.value(), target_value); enter(it.value(), target_value);
if (stack.size() == depth) if (stack.size() == depth)
@@ -7583,7 +7679,7 @@ public:
// append the "add" ops for brand-new keys collected when the // append the "add" ops for brand-new keys collected when the
// object was entered // object was entered
result.insert(result.end(), stack.back().added_ops.begin(), stack.back().added_ops.end()); diff_added(result, stack.back().added, current_path);
} }
// this array or object is done: continue with the one it is in // this array or object is done: continue with the one it is in
+404 -310
View File
File diff suppressed because it is too large. Load diff
+54
View File
@@ -323,3 +323,57 @@ TEST_CASE("copy constructor and destructor across value types")
CHECK(w.size() == 9); CHECK(w.size() == 9);
} }
} }
TEST_CASE_TEMPLATE("destructor of nested containers", Json, nlohmann::json, nlohmann::ordered_json)
{
SECTION("empty containers")
{
{
Json j = Json::array();
}
{
Json j = Json::object();
}
{
Json j = Json::array({Json::array(), Json::object(), Json::array({Json::array()})});
}
}
SECTION("leaves mixed with nested containers")
{
Json j = Json::object();
j["leaf"] = 1;
j["str"] = "text";
j["empty_a"] = Json::array();
j["empty_o"] = Json::object();
j["arr"] = Json::array({1, "a", Json::array({1, 2}), Json::object({{"k", Json::array({Json::object()})}}), nullptr});
j["obj"] = Json::object({{"x", Json::object({{"y", Json::array({1, Json::object({{"z", 1}})})}})}, {"w", 2}});
Json copy = j;
CHECK(copy == j);
j = Json::array({std::move(copy), j, 3, Json::array()});
}
SECTION("deep nesting")
{
for (int kind = 0; kind < 3; ++kind)
{
Json j = 1;
for (int i = 0; i < 5000; ++i)
{
switch (kind)
{
case 0:
j = Json::array({std::move(j)});
break;
case 1:
j = Json::object({{"k", std::move(j)}});
break;
default:
// siblings next to the nested child
j = (i % 2 == 0) ? Json::array({1, Json::array(), std::move(j)}) : Json::object({{"a", 1}, {"b", Json::object()}, {"c", std::move(j)}});
break;
}
}
}
}
}
+107
View File
@@ -10,6 +10,8 @@
#define JSON_TESTS_PRIVATE #define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <string>
#include <vector>
using nlohmann::json; using nlohmann::json;
TEST_CASE("iterators 1") TEST_CASE("iterators 1")
@@ -1628,3 +1630,108 @@ TEST_CASE("iterators 1")
} }
} }
} }
TEST_CASE_TEMPLATE("reverse iteration and member access", Json, nlohmann::json, nlohmann::ordered_json)
{
SECTION("array: -- and rbegin/rend")
{
Json j = {1, 2, 3, 4};
auto it = j.end();
std::vector<int> seen;
while (it != j.begin())
{
--it;
seen.push_back(it->template get<int>());
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
auto it2 = j.end();
auto old = it2--;
CHECK(old == j.end());
CHECK(*it2 == 4);
seen.clear();
for (auto r = j.rbegin(); r != j.rend(); ++r)
{
seen.push_back(r->template get<int>());
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
seen.clear();
for (auto r = j.crbegin(); r != j.crend(); ++r)
{
seen.push_back(*r);
}
CHECK(seen == std::vector<int> {4, 3, 2, 1});
}
SECTION("object: -- and rbegin/rend")
{
Json j = {{"a", 1}, {"b", 2}, {"c", 3}};
// order of keys depends on the object type; use the forward order as reference
std::vector<std::string> fwd;
for (auto it = j.begin(); it != j.end(); ++it)
{
fwd.push_back(it.key());
}
std::vector<std::string> rev(fwd.rbegin(), fwd.rend());
std::vector<std::string> seen;
auto it = j.end();
while (it != j.begin())
{
--it;
seen.push_back(it.key());
}
CHECK(seen == rev);
auto it2 = j.end();
auto old = it2--;
CHECK(old == j.end());
CHECK(it2.key() == rev.front());
seen.clear();
for (auto r = j.rbegin(); r != j.rend(); ++r)
{
seen.push_back(r.key());
}
CHECK(seen == rev);
}
SECTION("object: operator-> reads and writes the mapped value")
{
Json j = {{"a", 1}, {"s", "str"}};
auto it = j.find("s");
REQUIRE(it != j.end());
CHECK(it->is_string());
CHECK(it->size() == 1);
*it = Json::array({1, 2});
CHECK(it->is_array());
it->push_back(3);
CHECK(j["s"].size() == 3);
const Json& cj = j;
auto cit = cj.find("a");
REQUIRE(cit != cj.end());
CHECK(cit->is_number_integer());
CHECK(cit->template get<int>() == 1);
}
SECTION("primitives: -- and ->")
{
Json j = 42;
auto it = j.end();
--it;
CHECK(it == j.begin());
CHECK(it->template get<int>() == 42);
}
SECTION("comparing iterators of different containers throws")
{
Json a = {1, 2};
Json b = {1, 2};
CHECK_THROWS_WITH_AS(a.begin() == b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
CHECK_THROWS_WITH_AS(a.begin() < b.begin(), "[json.exception.invalid_iterator.212] cannot compare iterators of different containers", nlohmann::json::invalid_iterator);
}
}
+37
View File
@@ -2066,3 +2066,40 @@ TEST_CASE("JSON patch - every operation on ordered_json")
} }
} }
} }
TEST_CASE_TEMPLATE("JSON patch - diff round trip on hand-made documents", Json, nlohmann::json, nlohmann::ordered_json)
{
const auto roundtrip = [](const Json & a, const Json & b)
{
CAPTURE(a.dump())
CAPTURE(b.dump())
const Json p = Json::diff(a, b);
CHECK(a.patch(p) == b);
CHECK(Json::diff(b, b).empty());
};
SECTION("reordered keys")
{
const Json a = Json::parse(R"({"x":1,"y":2,"z":{"p":1,"q":2}})");
const Json b = Json::parse(R"({"z":{"q":2,"p":1},"y":2,"x":1})");
roundtrip(a, b);
roundtrip(b, a);
}
SECTION("nested add, remove and replace")
{
const Json a = Json::parse(R"({"a":[1,2,{"k":"v","gone":true}],"b":{"c":{"d":1}},"e":"s"})");
const Json b = Json::parse(R"({"a":[1,3,{"k":"w","new":[]}],"b":{"c":{"d":[1,2]}},"f":null})");
roundtrip(a, b);
roundtrip(b, a);
}
SECTION("arrays of different length and type changes")
{
roundtrip(Json::parse("[1,2,3,4,5]"), Json::parse("[1,3]"));
roundtrip(Json::parse("[1]"), Json::parse("[1,[2],{\"a\":3}]"));
roundtrip(Json::parse("[]"), Json::parse("{}"));
roundtrip(Json::parse("{\"a\":[]}"), Json::parse("{\"a\":{}}"));
roundtrip(Json(1), Json::parse("[1]"));
}
}