From 21bc758b896293e127f5f5a2f14e6e10abd663b6 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Fri, 23 Oct 2026 16:00:12 +0200 Subject: [PATCH] from_json: number-first dispatch for arithmetic types, loop for std::array (#5891) from_json(ArithmeticType&) now reads numbers through get_number_value() and handles a boolean on a cold path, which puts the three number types first in the per-element type dispatch. get>() always took the identity_tag overload, a pack expansion of N calls j.at(i).get() that clang unrolls into straight-line code (about 28,000 instructions for N = 1000), so its speed depended on inlining decisions in that body. For trivial element types, which cannot tell default construction plus assignment from construction in place, it now fills a std::array in a loop. That loop, shared with get_to(), checks the size once and reads the inner array directly; a short array still converts the elements it has and then throws the out_of_range error at() would throw. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/conversions/from_json.hpp | 103 ++++++++++-------- single_include/nlohmann/json.hpp | 103 ++++++++++-------- tests/src/unit-constructor1.cpp | 22 ++++ 3 files changed, 140 insertions(+), 88 deletions(-) diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index fd2361d81..93dc1637f 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -8,7 +8,7 @@ #pragma once -#include // transform +#include // min, transform #include // array #include // forward_list #include // inserter, front_inserter, end @@ -256,15 +256,30 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: arr = *j.template get_ptr(); } +// fill a std::array from the JSON array j: the size is checked once and the +// elements are read in order, so a short array converts the elements it has and +// then throws the out_of_range error at() would throw for the first missing one +template +void from_json_std_array(const BasicJsonType& j, std::array& arr) +{ + const auto& inner_array = *j.template get_ptr(); + const std::size_t size = (std::min)(N, inner_array.size()); + for (std::size_t i = 0; i < size; ++i) + { + arr[i] = inner_array[i].template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(j.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { - for (std::size_t i = 0; i < N; ++i) - { - arr[i] = j.at(i).template get(); - } + from_json_std_array(j, arr); } // reserve() is called through this pair (used for arrays and objects alike) @@ -347,6 +362,35 @@ std::array from_json_inplace_array_impl(const BasicJsonType& return { { j.at(Idx).template get()... } }; } +// std::is_trivial is deprecated in C++26 (see binary_writer::to_char_type) +#ifdef JSON_HAS_CPP_26 + template + using is_trivial_element = conjunction, std::is_trivially_default_constructible>; +#else + template + using is_trivial_element = std::is_trivial; +#endif + +// the element type may not be default constructible: construct every element +// in place (a pack expansion that is unrolled N times) +template < typename BasicJsonType, typename T, std::size_t N > +std::array from_json_inplace_array(const BasicJsonType& j, identity_tag> tag, + std::false_type /*is_trivial_element*/) +{ + return from_json_inplace_array_impl(j, tag, make_index_sequence {}); +} + +// a trivial element type cannot tell default construction plus assignment from +// construction in place, so use a loop instead of unrolling N conversions +template < typename BasicJsonType, typename T, std::size_t N > +std::array from_json_inplace_array(const BasicJsonType& j, identity_tag> /*unused*/, + std::true_type /*is_trivial_element*/) +{ + std::array arr; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): every element is assigned or an exception is thrown + from_json_std_array(j, arr); + return arr; +} + template < typename BasicJsonType, typename T, std::size_t N > auto from_json(const BasicJsonType& j, identity_tag> tag) -> decltype(from_json_inplace_array_impl(j, tag, make_index_sequence {})) @@ -356,7 +400,7 @@ auto from_json(const BasicJsonType& j, identity_tag> tag) throw_type_must_be("array", j); } - return from_json_inplace_array_impl(j, tag, make_index_sequence {}); + return from_json_inplace_array(j, tag, is_trivial_element {}); } template @@ -437,46 +481,17 @@ template < typename BasicJsonType, typename ArithmeticType, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { - // the number cases repeat get_number_value() on purpose: calling it here - // changes the inlining of get() in the std::array conversion, which then - // runs 2.4x slower (GetArray benchmark, #5891); unlike get_arithmetic_value(), - // a boolean is accepted - switch (static_cast(j)) + if (get_number_value(j, val)) { - case value_t::number_unsigned: - { - // never null: a basic_json whose number_unsigned_t is number_integer_t - // stores no number_unsigned values (#2573) - const auto* ptr = j.template get_ptr(); - JSON_ASSERT(ptr != nullptr); - val = static_cast(*ptr); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::boolean: - { - val = static_cast(*j.template get_ptr()); - break; - } - - case value_t::null: - case value_t::object: - case value_t::array: - case value_t::string: - case value_t::binary: - case value_t::discarded: - default: - throw_type_must_be("number", j); + return; } + + // unlike get_arithmetic_value(), a boolean is accepted here + if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) + { + throw_type_must_be("number", j); + } + val = static_cast(*j.template get_ptr()); } template diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 92468f89e..ab0420e18 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -284,7 +284,7 @@ -#include // transform +#include // min, transform #include // array #include // forward_list #include // inserter, front_inserter, end @@ -6279,15 +6279,30 @@ inline void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType: arr = *j.template get_ptr(); } +// fill a std::array from the JSON array j: the size is checked once and the +// elements are read in order, so a short array converts the elements it has and +// then throws the out_of_range error at() would throw for the first missing one +template +void from_json_std_array(const BasicJsonType& j, std::array& arr) +{ + const auto& inner_array = *j.template get_ptr(); + const std::size_t size = (std::min)(N, inner_array.size()); + for (std::size_t i = 0; i < size; ++i) + { + arr[i] = inner_array[i].template get(); + } + if (JSON_HEDLEY_UNLIKELY(size < N)) + { + static_cast(j.at(size)); + } +} + template auto from_json_array_impl(const BasicJsonType& j, std::array& arr, priority_tag<2> /*unused*/) -> decltype(j.template get(), void()) { - for (std::size_t i = 0; i < N; ++i) - { - arr[i] = j.at(i).template get(); - } + from_json_std_array(j, arr); } // reserve() is called through this pair (used for arrays and objects alike) @@ -6370,6 +6385,35 @@ std::array from_json_inplace_array_impl(const BasicJsonType& return { { j.at(Idx).template get()... } }; } +// std::is_trivial is deprecated in C++26 (see binary_writer::to_char_type) +#ifdef JSON_HAS_CPP_26 + template + using is_trivial_element = conjunction, std::is_trivially_default_constructible>; +#else + template + using is_trivial_element = std::is_trivial; +#endif + +// the element type may not be default constructible: construct every element +// in place (a pack expansion that is unrolled N times) +template < typename BasicJsonType, typename T, std::size_t N > +std::array from_json_inplace_array(const BasicJsonType& j, identity_tag> tag, + std::false_type /*is_trivial_element*/) +{ + return from_json_inplace_array_impl(j, tag, make_index_sequence {}); +} + +// a trivial element type cannot tell default construction plus assignment from +// construction in place, so use a loop instead of unrolling N conversions +template < typename BasicJsonType, typename T, std::size_t N > +std::array from_json_inplace_array(const BasicJsonType& j, identity_tag> /*unused*/, + std::true_type /*is_trivial_element*/) +{ + std::array arr; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): every element is assigned or an exception is thrown + from_json_std_array(j, arr); + return arr; +} + template < typename BasicJsonType, typename T, std::size_t N > auto from_json(const BasicJsonType& j, identity_tag> tag) -> decltype(from_json_inplace_array_impl(j, tag, make_index_sequence {})) @@ -6379,7 +6423,7 @@ auto from_json(const BasicJsonType& j, identity_tag> tag) throw_type_must_be("array", j); } - return from_json_inplace_array_impl(j, tag, make_index_sequence {}); + return from_json_inplace_array(j, tag, is_trivial_element {}); } template @@ -6460,46 +6504,17 @@ template < typename BasicJsonType, typename ArithmeticType, int > = 0 > inline void from_json(const BasicJsonType& j, ArithmeticType& val) { - // the number cases repeat get_number_value() on purpose: calling it here - // changes the inlining of get() in the std::array conversion, which then - // runs 2.4x slower (GetArray benchmark, #5891); unlike get_arithmetic_value(), - // a boolean is accepted - switch (static_cast(j)) + if (get_number_value(j, val)) { - case value_t::number_unsigned: - { - // never null: a basic_json whose number_unsigned_t is number_integer_t - // stores no number_unsigned values (#2573) - const auto* ptr = j.template get_ptr(); - JSON_ASSERT(ptr != nullptr); - val = static_cast(*ptr); - break; - } - case value_t::number_integer: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::number_float: - { - val = static_cast(*j.template get_ptr()); - break; - } - case value_t::boolean: - { - val = static_cast(*j.template get_ptr()); - break; - } - - case value_t::null: - case value_t::object: - case value_t::array: - case value_t::string: - case value_t::binary: - case value_t::discarded: - default: - throw_type_must_be("number", j); + return; } + + // unlike get_arithmetic_value(), a boolean is accepted here + if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) + { + throw_type_must_be("number", j); + } + val = static_cast(*j.template get_ptr()); } template diff --git a/tests/src/unit-constructor1.cpp b/tests/src/unit-constructor1.cpp index 600eb0069..a1778d9e0 100644 --- a/tests/src/unit-constructor1.cpp +++ b/tests/src/unit-constructor1.cpp @@ -361,6 +361,28 @@ TEST_CASE("constructors") CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); + CHECK_THROWS_WITH_AS((j.get>()), "[json.exception.type_error.302] type must be string, but is number", json::type_error&); + + // the elements are converted in order, so a type error comes before a missing element + json const j2{1, "two"}; + CHECK_THROWS_WITH_AS((j2.get>()), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); + std::array a{{0, 0, 0}}; + CHECK_THROWS_WITH_AS(j2.get_to(a), "[json.exception.type_error.302] type must be number, but is string", json::type_error&); + CHECK_THROWS_WITH_AS(j.get_to(a), "[json.exception.out_of_range.401] array index 1 is out of range", json::out_of_range&); + CHECK(a[0] == 1); + + json const j3{"one", "two"}; + CHECK_THROWS_WITH_AS((j3.get>()), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&); + } + + SECTION("std::array from a longer array or from booleans") + { + json const j{1, 2.5, true, 4u}; + CHECK(j.get>() == (std::array {{1, 2, 1}})); + std::array a{{0, 0, 0, 0}}; + j.get_to(a); + CHECK(a == (std::array {{1, 2, 1, 4}})); + CHECK(j.get>().empty()); } SECTION("std::forward_list")