Compare commits

...
Author SHA1 Message Date
Niels Lohmann 73b0a3aba3 Drop the null_result parameter JSON_IMPLEMENT_OPERATOR no longer uses
Without the same-type scalar cases, the macro body has no use for it, and
JSON_IMPLEMENT_SCALAR_CASES is now used only by JSON_IMPLEMENT_SCALAR_FAST.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 10:05:55 +01:00
Niels Lohmann 7196bc5b22 iter_impl: keep the type-dependent initialization in the constructor
The check is not needed for correctness (m_it is value-initialized), but
without it GCC 13 generates slower iteration loops: a recursive walk over
a parsed document takes 0.5-2.7 % longer (callgrind: +1.9 % instructions
on twitter.json, +0.95 % on canada.json). Clang generates identical code
either way. See the measurements in #6005.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 10:05:54 +01:00
Niels Lohmann 2ac0851157 basic_json, iterators, serializer: remove checks that invariants make dead
Branch coverage of the test suite and of 24k mutated inputs showed
these conditions as never taken; each is implied by the code around it:

- copy_object_level(): `src_it != src_object.cend()`; dst was built from
  exactly src_object.size() unique keys and src_it advances at most once
  per member
- the general comparison operators (equal_general, less_general,
  three_way_general): the five same-type scalar cases, which the inline
  fast path of every operator handles before calling them (now an
  assert); and the NaN half of compares_unordered() in the mixed-type
  branch, reached only after all six number pairs returned, where only
  a discarded operand is unordered
- serializer, ensure_ascii: the reset of `bytes` under error_handler_t::keep
  and the surrogate-pair escape in the UTF8_ACCEPT case; every
  well-formed multi-byte character is escaped by write_u_escapes() before
  decode() sees it, so decode() only ever accepts DEL here, a single byte
  of which nothing was buffered (now asserts)
- iter_impl(pointer): the if/else chain assigned value-initialized
  iterators to the value-initialized m_it

No change in behaviour: a build that aborts on each removed side passes
the test suite, the fuzz corpora and 23.2M dumps of all 1-, 2- and
boundary 4-byte strings in both ensure_ascii modes.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-02 09:41:01 +01:00
4 changed files with 44 additions and 42 deletions

No files matched your search

@@ -108,6 +108,9 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
// m_it is already value-initialized, so this is not needed for
// correctness; it is kept because GCC generates faster loops with it
// (iterating a parsed document is 0.5-2.7 % slower without it, #6005)
if (m_object->is_object()) if (m_object->is_object())
{ {
m_it.object_iterator = typename object_t::iterator(); m_it.object_iterator = typename object_t::iterator();
+5 -12
View File
@@ -1113,21 +1113,14 @@ class serializer
{ {
// the code point is at least 0x60 (smaller ones are handled // the code point is at least 0x60 (smaller ones are handled
// above) and is not printable ASCII (which the scanner // above) and is not printable ASCII (which the scanner
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII // copies). Under EnsureAscii it is DEL (0x7F): every
// character // well-formed multi-byte character is escaped by
// write_u_escapes() above, so decode() only ever accepts a
// single byte here, and nothing of it was buffered
if (EnsureAscii) if (EnsureAscii)
{ {
JSON_ASSERT(codepoint == 0x7F); JSON_ASSERT(codepoint == 0x7F);
if (error_handler == error_handler_t::keep) JSON_ASSERT(bytes == bytes_after_last_accept);
{
// this character was buffered as raw bytes below
// in case it turned out to be part of an
// ill-formed sequence (which is kept as is); now
// that it decoded to a well-formed code point,
// undo that and \u-escape it like any other
// character instead
bytes = bytes_after_last_accept;
}
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint)); write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
} }
else else
+14 -9
View File
@@ -1576,7 +1576,7 @@ public:
auto src_it = src_object.cbegin(); auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object) for (auto& element : *dst.m_data.m_value.object)
{ {
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) if (JSON_HEDLEY_LIKELY(copy_keys_equal(src_it->first, element.first)))
{ {
copy_shallow(src_it->second, element.second, worklist); copy_shallow(src_it->second, element.second, worklist);
++src_it; ++src_it;
@@ -5353,8 +5353,7 @@ public:
/// @name lexicographical comparison operators /// @name lexicographical comparison operators
/// @{ /// @{
// the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR // the same-type cases for non-owning scalars, used by JSON_IMPLEMENT_SCALAR_FAST
// and JSON_IMPLEMENT_SCALAR_FAST
#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ #define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \
case value_t::null: \ case value_t::null: \
return (null_result); \ return (null_result); \
@@ -5376,7 +5375,7 @@ public:
// because any negative signed value is smaller than any unsigned value. // because any negative signed value is smaller than any unsigned value.
// Otherwise, the non-negative signed value is cast to unsigned before the // Otherwise, the non-negative signed value is cast to unsigned before the
// comparison to avoid wraparound. // comparison to avoid wraparound.
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \ #define JSON_IMPLEMENT_OPERATOR(op, unordered_result, default_result, deep_result, may_descend) \
const auto lhs_type = lhs.type(); \ const auto lhs_type = lhs.type(); \
const auto rhs_type = rhs.type(); \ const auto rhs_type = rhs.type(); \
\ \
@@ -5404,7 +5403,14 @@ public:
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
} \ } \
\ \
JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ /* same-type scalars never get here: the inline fast path of the operator handles them */ \
case value_t::null: \
case value_t::boolean: \
case value_t::number_integer: \
case value_t::number_unsigned: \
case value_t::number_float: \
JSON_ASSERT(false); /* NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) */ \
return (unordered_result); \
\ \
case value_t::string: \ case value_t::string: \
return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \
@@ -5445,7 +5451,7 @@ public:
? (number_integer_t(-1) op number_integer_t(1)) \ ? (number_integer_t(-1) op number_integer_t(1)) \
: (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ : (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \
} \ } \
else if(compares_unordered(lhs, rhs))\ else if(lhs_type == value_t::discarded || rhs_type == value_t::discarded)\
{\ {\
return (unordered_result);\ return (unordered_result);\
}\ }\
@@ -5513,7 +5519,7 @@ public:
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
#endif #endif
JSON_IMPLEMENT_OPERATOR( ==, true, false, false, JSON_IMPLEMENT_OPERATOR( ==, false, false,
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true) compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
@@ -5527,7 +5533,6 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. // we compare types.
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
std::partial_ordering::equivalent,
std::partial_ordering::unordered, std::partial_ordering::unordered,
lhs_type <=> rhs_type, // *NOPAD* lhs_type <=> rhs_type, // *NOPAD*
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true) to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
@@ -5539,7 +5544,7 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. Note we have to call the operator explicitly, // we compare types. Note we have to call the operator explicitly,
// because MSVC has problems otherwise. // because MSVC has problems otherwise.
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), JSON_IMPLEMENT_OPERATOR( <, false, operator<(lhs_type, rhs_type),
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false) compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
} }
#endif #endif
+22 -21
View File
@@ -24064,6 +24064,9 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci
{ {
JSON_ASSERT(m_object != nullptr); JSON_ASSERT(m_object != nullptr);
// m_it is already value-initialized, so this is not needed for
// correctness; it is kept because GCC generates faster loops with it
// (iterating a parsed document is 0.5-2.7 % slower without it, #6005)
if (m_object->is_object()) if (m_object->is_object())
{ {
m_it.object_iterator = typename object_t::iterator(); m_it.object_iterator = typename object_t::iterator();
@@ -36267,21 +36270,14 @@ class serializer
{ {
// the code point is at least 0x60 (smaller ones are handled // the code point is at least 0x60 (smaller ones are handled
// above) and is not printable ASCII (which the scanner // above) and is not printable ASCII (which the scanner
// copies): it is DEL (0x7F) under EnsureAscii or a non-ASCII // copies). Under EnsureAscii it is DEL (0x7F): every
// character // well-formed multi-byte character is escaped by
// write_u_escapes() above, so decode() only ever accepts a
// single byte here, and nothing of it was buffered
if (EnsureAscii) if (EnsureAscii)
{ {
JSON_ASSERT(codepoint == 0x7F); JSON_ASSERT(codepoint == 0x7F);
if (error_handler == error_handler_t::keep) JSON_ASSERT(bytes == bytes_after_last_accept);
{
// this character was buffered as raw bytes below
// in case it turned out to be part of an
// ill-formed sequence (which is kept as is); now
// that it decoded to a well-formed code point,
// undo that and \u-escape it like any other
// character instead
bytes = bytes_after_last_accept;
}
write_u_escape(bytes, static_cast<std::uint16_t>(codepoint)); write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
} }
else else
@@ -39305,7 +39301,7 @@ public:
auto src_it = src_object.cbegin(); auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object) for (auto& element : *dst.m_data.m_value.object)
{ {
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first))) if (JSON_HEDLEY_LIKELY(copy_keys_equal(src_it->first, element.first)))
{ {
copy_shallow(src_it->second, element.second, worklist); copy_shallow(src_it->second, element.second, worklist);
++src_it; ++src_it;
@@ -43082,8 +43078,7 @@ public:
/// @name lexicographical comparison operators /// @name lexicographical comparison operators
/// @{ /// @{
// the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR // the same-type cases for non-owning scalars, used by JSON_IMPLEMENT_SCALAR_FAST
// and JSON_IMPLEMENT_SCALAR_FAST
#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ #define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \
case value_t::null: \ case value_t::null: \
return (null_result); \ return (null_result); \
@@ -43105,7 +43100,7 @@ public:
// because any negative signed value is smaller than any unsigned value. // because any negative signed value is smaller than any unsigned value.
// Otherwise, the non-negative signed value is cast to unsigned before the // Otherwise, the non-negative signed value is cast to unsigned before the
// comparison to avoid wraparound. // comparison to avoid wraparound.
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \ #define JSON_IMPLEMENT_OPERATOR(op, unordered_result, default_result, deep_result, may_descend) \
const auto lhs_type = lhs.type(); \ const auto lhs_type = lhs.type(); \
const auto rhs_type = rhs.type(); \ const auto rhs_type = rhs.type(); \
\ \
@@ -43133,7 +43128,14 @@ public:
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
} \ } \
\ \
JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ /* same-type scalars never get here: the inline fast path of the operator handles them */ \
case value_t::null: \
case value_t::boolean: \
case value_t::number_integer: \
case value_t::number_unsigned: \
case value_t::number_float: \
JSON_ASSERT(false); /* NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) */ \
return (unordered_result); \
\ \
case value_t::string: \ case value_t::string: \
return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \
@@ -43174,7 +43176,7 @@ public:
? (number_integer_t(-1) op number_integer_t(1)) \ ? (number_integer_t(-1) op number_integer_t(1)) \
: (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \ : (static_cast<number_unsigned_t>(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_unsigned); \
} \ } \
else if(compares_unordered(lhs, rhs))\ else if(lhs_type == value_t::discarded || rhs_type == value_t::discarded)\
{\ {\
return (unordered_result);\ return (unordered_result);\
}\ }\
@@ -43242,7 +43244,7 @@ public:
JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_DIAGNOSTIC_PUSH
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
#endif #endif
JSON_IMPLEMENT_OPERATOR( ==, true, false, false, JSON_IMPLEMENT_OPERATOR( ==, false, false,
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true) compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
#ifdef __GNUC__ #ifdef __GNUC__
JSON_HEDLEY_DIAGNOSTIC_POP JSON_HEDLEY_DIAGNOSTIC_POP
@@ -43256,7 +43258,6 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. // we compare types.
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
std::partial_ordering::equivalent,
std::partial_ordering::unordered, std::partial_ordering::unordered,
lhs_type <=> rhs_type, // *NOPAD* lhs_type <=> rhs_type, // *NOPAD*
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true) to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
@@ -43268,7 +43269,7 @@ public:
// default_result is used if we cannot compare values. In that case, // default_result is used if we cannot compare values. In that case,
// we compare types. Note we have to call the operator explicitly, // we compare types. Note we have to call the operator explicitly,
// because MSVC has problems otherwise. // because MSVC has problems otherwise.
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), JSON_IMPLEMENT_OPERATOR( <, false, operator<(lhs_type, rhs_type),
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false) compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
} }
#endif #endif