diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 8bc36ccc7..03bee9eac 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -160,6 +160,18 @@ class input_stream_adapter template class iterator_input_adapter { + // Whether the number of elements between two positions can be computed in + // O(1): either the iterator and the sentinel have the same type (plain + // std::distance) or, in C++20, the sentinel is a sized sentinel for the + // iterator (std::ranges::distance), e.g. std::default_sentinel_t paired + // with std::counted_iterator. + static constexpr bool sentinel_is_sized = +#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) + std::is_same::value || std::sized_sentinel_for; +#else + std::is_same::value; +#endif + public: using char_type = typename std::iterator_traits::value_type; @@ -171,7 +183,7 @@ class iterator_input_adapter // in wide_string_input_adapter, which does not expose this). static constexpr bool supports_seek = std::is_same::iterator_category, std::random_access_iterator_tag>::value - && std::is_same::value + && sentinel_is_sized && sizeof(char_type) == 1; iterator_input_adapter(IteratorType first, SentinelType last) @@ -219,25 +231,34 @@ class iterator_input_adapter private: // whether IteratorType refers to a contiguous range and therefore supports // a std::memcpy fast path (pointers always do; in C++20 we can also detect - // library iterators such as those of std::vector and std::string). - // Computing the available element count needs either same-type iterators - // (plain std::distance) or, in C++20, a sized sentinel (std::ranges::distance), - // e.g. std::counted_iterator paired with std::default_sentinel_t. - static constexpr bool iterator_is_contiguous = + // library iterators such as those of std::vector and std::string). The + // available element count must also be computable in O(1), hence + // sentinel_is_sized. + static constexpr bool iterator_is_contiguous = sentinel_is_sized && #if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) - (std::is_same::value || std::sized_sentinel_for) - && (std::contiguous_iterator || std::is_pointer::value); + (std::contiguous_iterator || std::is_pointer::value); #else - std::is_same::value && std::is_pointer::value; + std::is_pointer::value; #endif + // number of unread elements in [current, end) + std::size_t remaining_count() const + { +#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) + // std::ranges::distance also supports sized sentinels of a different + // type (e.g. std::counted_iterator + std::default_sentinel_t) + return static_cast(std::ranges::distance(current, end)); +#else + return static_cast(std::distance(current, end)); +#endif + } + public: // Whether the remaining input is a single contiguous block of 1-byte // elements that the lexer can inspect directly (used for the SWAR string - // fast path). Restricted to same-type iterator/sentinel pairs so that plain - // std::distance/std::advance are well-defined in all standards. + // fast path). static constexpr bool supports_bulk_scan = - iterator_is_contiguous && std::is_same::value && sizeof(char_type) == 1; + iterator_is_contiguous && sizeof(char_type) == 1; // Pointer to the next unread element; only valid when bulk_remaining() > 0. const char_type* bulk_data() const @@ -248,7 +269,7 @@ class iterator_input_adapter // Number of unread elements available as one contiguous block. std::size_t bulk_remaining() const { - return static_cast(std::distance(current, end)); + return remaining_count(); } // Consume @a n elements previously inspected via bulk_data(). @@ -263,13 +284,7 @@ class iterator_input_adapter std::size_t get_elements_impl(T* dest, std::size_t count, std::true_type /*contiguous*/) { const std::size_t wanted = count * sizeof(T); -#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) - // std::ranges::distance also supports sized sentinels of a different - // type (e.g. std::counted_iterator + std::default_sentinel_t) - const std::size_t available = static_cast(std::ranges::distance(current, end)) * sizeof(char_type); -#else - const std::size_t available = static_cast(std::distance(current, end)) * sizeof(char_type); -#endif + const std::size_t available = remaining_count() * sizeof(char_type); const std::size_t copied = (std::min)(wanted, available); if (JSON_HEDLEY_LIKELY(copied != 0)) { diff --git a/tests/src/unit-user_defined_input.cpp b/tests/src/unit-user_defined_input.cpp index 823e82862..fe15fb4b6 100644 --- a/tests/src/unit-user_defined_input.cpp +++ b/tests/src/unit-user_defined_input.cpp @@ -228,6 +228,58 @@ TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t") const std::counted_iterator first2(json_str.begin(), len); CHECK(json::accept(first2, std::default_sentinel)); } + +TEST_CASE("std::counted_iterator reaches the contiguous fast paths") +{ + // A sized sentinel makes the remaining element count computable in O(1), so + // std::counted_iterator over a contiguous iterator must reach the same bulk + // string/number scanners as a plain pointer - not just the byte-at-a-time + // fallback (see #5268 for the equivalent memcpy fast path). + using adapter_type = nlohmann::detail::iterator_input_adapter, std::default_sentinel_t>; + CHECK(adapter_type::supports_bulk_scan); + CHECK(adapter_type::supports_seek); + + // exercise every fast path: long ASCII run, multibyte UTF-8, escapes, and + // integer/floating-point numbers + const std::string json_str = + R"({"ascii":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",)" + "\"utf8\":\"\xe4\xb8\xad\xe6\x96\x87\xf0\x9f\x98\x80\xc3\xa9\"," + R"("escaped":"aéb\n\\","ints":[0,-1,18446744073709551615,-9223372036854775808],)" + R"("floats":[1.5,-2.25e3,0.30000000000000004]})"; + const auto len = static_cast>(json_str.size()); + + const std::counted_iterator first(json_str.data(), len); + const json j = json::parse(first, std::default_sentinel); + + // parsing through the pointer adapter must give exactly the same result + CHECK(j == json::parse(json_str)); + + // and errors must still be reported identically + const std::string bad = "[01\n]"; + const std::counted_iterator bad_first(bad.data(), static_cast>(bad.size())); + std::string counted_what; + std::string string_what; + try + { + const json j = json::parse(bad_first, std::default_sentinel); + static_cast(j); + } + catch (const json::parse_error& e) + { + counted_what = e.what(); + } + try + { + const json j = json::parse(bad); + static_cast(j); + } + catch (const json::parse_error& e) + { + string_what = e.what(); + } + CHECK_FALSE(counted_what.empty()); + CHECK(counted_what == string_what); +} #endif } // namespace