Compare commits

..
Author SHA1 Message Date
Claude d479d34677 Un-nest the run-length ternary in the string escaper
clang-tidy's readability-avoid-nested-conditional-operator (newly enforced
by the ci_clang_tidy job's clang) rejected the nested ?: that picked the
run length: EnsureAscii ? (is_ascii_copyable(...) ? scan : 0) : bulk.

Compute it in an immediately-invoked lambda instead. The value stays const
and the laziness is unchanged - the ascii scan still runs only on the
ensure_ascii path when the first byte is copyable, and string_bulk_run only
on the other path (EnsureAscii is a template bool, so the dead branch is
folded away). Output is byte-for-byte identical; single_include regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1oJ2ggRHS37zeVe94QTA1
Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:42:18 +00:00
Niels LohmannandClaude 09ae666612 Take the output adapter by reference at the serializer ctor
Per review: the serializer still holds the adapter as a non-owning
pointer, but the constructor now takes output_adapter_protocol<char>&
and takes its address internally, so every call site passes a
reference. A reference cannot be null and reads as a borrow, which
makes the lifetime contract harder to get wrong than handing over a
raw pointer. The stored member and the write path are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAYM1qhSA2FDaDcGfPW3fG
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-02 22:29:20 +02:00
Claude 78f71358c1 Update the serializer test for the non-owning adapter ctor
check_escaped constructed the serializer with output_adapter<char>(ss),
which produced the old owning output_adapter_t. The ctor now takes a
non-owning output_adapter_protocol<char>*, so build the concrete
output_stream_adapter on the stack and pass its address, matching how
dump() and operator<< now call it.

Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:29:19 +02:00
Claude f707a2f997 Stop dump() from heap-allocating its output adapter per call
The serializer held its output sink as output_adapter_t<char>
(a std::shared_ptr<output_adapter_protocol<char>>), which dump() and
operator<< built via make_shared -- one heap allocation per call for a
sink that only wraps a reference to the caller's string or stream.

Hold the sink as a non-owning output_adapter_protocol<char>* instead and
construct the concrete adapter on the stack at the call site. The write
path (o->write_characters) is unchanged, so output is byte-for-byte
identical; a compact dump() of a small object drops from 2 heap
allocations to 1 (only the returned string remains), ~3% faster.

Completes the per-call allocation cleanup on this branch, which already
removed the indent_string buffer (both were reported in #5413).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1oJ2ggRHS37zeVe94QTA1
Signed-off-by: Claude <noreply@anthropic.com>
2026-09-02 22:29:19 +02:00
Niels Lohmann 3426a41391 Do not scan for a copyable run that cannot exist
Under ensure_ascii, dump_escaped() calls find_ascii_copyable_run() at every
character boundary. When the text is dense non-ASCII - CJK, where every byte
is >= 0x80 - the scanner stops on its first byte and returns zero, so its SWAR
block runs once per character and buys nothing, on top of the escaping that
still has to happen afterwards.

A run can only be non-empty when the first byte is one the scanner may copy,
so test that single byte before calling it. Runs that do exist are found
exactly as before, so the bulk-copy win is unchanged; only the calls that were
always going to return zero are skipped.

Output is unchanged: the dump digest over canada/citm/twitter, in compact,
pretty and ensure_ascii form, matches develop byte for byte.

  dump(ensure_ascii=true)   develop    before     after
  CJK text                   3.54ms    4.25ms    3.36ms
  CJK, no ASCII at all       3.09ms    4.02ms    3.02ms
  Latin-1-ish text           4.39ms    3.04ms    2.93ms
  plain ASCII                3.92ms    0.80ms    0.79ms

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-02 22:28:50 +02:00
4 changed files with 40 additions and 31 deletions
+13 -11
View File
@@ -63,13 +63,14 @@ class serializer
public:
/*!
@param[in] s output stream to serialize to
@param[in] s output adapter to serialize to; not owned by the serializer,
so it must outlive it (it lives at the call site)
@param[in] ichar indentation character to use
@param[in] error_handler_ how to react on decoding errors
*/
serializer(output_adapter_t<char> s, const char ichar,
serializer(output_adapter_protocol<char>& s, const char ichar,
error_handler_t error_handler_ = error_handler_t::strict)
: o(std::move(s))
: o(&s)
, loc(std::localeconv())
, thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->thousands_sep)))
, decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->decimal_point)))
@@ -849,15 +850,16 @@ class serializer
// for the scan. Without it, text whose characters all have to be
// escaped - CJK under ensure_ascii, where every byte is >= 0x80 -
// runs the scanner once per character only to be told zero.
std::size_t run = 0;
if (!EnsureAscii)
const std::size_t run = [&]() -> std::size_t
{
run = string_bulk_run(data + i, s.size() - i);
}
else if (is_ascii_copyable(data[i]))
if (EnsureAscii)
{
run = find_ascii_copyable_run(data + i, s.size() - i);
return is_ascii_copyable(data[i])
? find_ascii_copyable_run(data + i, s.size() - i)
: 0;
}
return string_bulk_run(data + i, s.size() - i);
}();
if (run != 0)
{
// emit any bytes still pending in string_buffer first to
@@ -1688,8 +1690,8 @@ class serializer
}
private:
/// the output of the serializer
output_adapter_t<char> o = nullptr;
/// the output of the serializer (non-owning; the adapter lives at the call site)
output_adapter_protocol<char>* o = nullptr;
/// a (hopefully) large enough character buffer
std::array<char, 64> number_buffer{{}};
+4 -2
View File
@@ -1341,7 +1341,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const error_handler_t error_handler = error_handler_t::strict) const
{
string_t result;
serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);
detail::output_string_adapter<char, string_t> string_adapter(result);
serializer s(string_adapter, indent_char, error_handler);
if (indent >= 0)
{
@@ -4055,7 +4056,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
o.width(0);
// do the actual serialization
serializer s(detail::output_adapter<char>(o), o.fill());
detail::output_stream_adapter<char> stream_adapter(o);
serializer s(stream_adapter, o.fill());
s.dump(j, pretty_print, false, static_cast<unsigned int>(indentation));
return o;
}
+17 -13
View File
@@ -21123,13 +21123,14 @@ class serializer
public:
/*!
@param[in] s output stream to serialize to
@param[in] s output adapter to serialize to; not owned by the serializer,
so it must outlive it (it lives at the call site)
@param[in] ichar indentation character to use
@param[in] error_handler_ how to react on decoding errors
*/
serializer(output_adapter_t<char> s, const char ichar,
serializer(output_adapter_protocol<char>& s, const char ichar,
error_handler_t error_handler_ = error_handler_t::strict)
: o(std::move(s))
: o(&s)
, loc(std::localeconv())
, thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->thousands_sep)))
, decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits<char>::to_char_type(* (loc->decimal_point)))
@@ -21909,15 +21910,16 @@ class serializer
// for the scan. Without it, text whose characters all have to be
// escaped - CJK under ensure_ascii, where every byte is >= 0x80 -
// runs the scanner once per character only to be told zero.
std::size_t run = 0;
if (!EnsureAscii)
const std::size_t run = [&]() -> std::size_t
{
run = string_bulk_run(data + i, s.size() - i);
}
else if (is_ascii_copyable(data[i]))
if (EnsureAscii)
{
run = find_ascii_copyable_run(data + i, s.size() - i);
return is_ascii_copyable(data[i])
? find_ascii_copyable_run(data + i, s.size() - i)
: 0;
}
return string_bulk_run(data + i, s.size() - i);
}();
if (run != 0)
{
// emit any bytes still pending in string_buffer first to
@@ -22748,8 +22750,8 @@ class serializer
}
private:
/// the output of the serializer
output_adapter_t<char> o = nullptr;
/// the output of the serializer (non-owning; the adapter lives at the call site)
output_adapter_protocol<char>* o = nullptr;
/// a (hopefully) large enough character buffer
std::array<char, 64> number_buffer{{}};
@@ -24451,7 +24453,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
const error_handler_t error_handler = error_handler_t::strict) const
{
string_t result;
serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);
detail::output_string_adapter<char, string_t> string_adapter(result);
serializer s(string_adapter, indent_char, error_handler);
if (indent >= 0)
{
@@ -27165,7 +27168,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
o.width(0);
// do the actual serialization
serializer s(detail::output_adapter<char>(o), o.fill());
detail::output_stream_adapter<char> stream_adapter(o);
serializer s(stream_adapter, o.fill());
s.dump(j, pretty_print, false, static_cast<unsigned int>(indentation));
return o;
}
+2 -1
View File
@@ -98,7 +98,8 @@ void check_escaped(const char* original, const char* escaped = "", bool ensure_a
void check_escaped(const char* original, const char* escaped, const bool ensure_ascii)
{
std::stringstream ss;
json::serializer s(nlohmann::detail::output_adapter<char>(ss), ' ');
nlohmann::detail::output_stream_adapter<char> adapter(ss);
json::serializer s(adapter, ' ');
s.dump_escaped(original, ensure_ascii);
s.flush(); // dump_escaped writes into the serializer's internal buffer
CHECK(ss.str() == escaped);