diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index cc5554c9e..bd19d32a8 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -620,23 +620,45 @@ typename iterator_input_adapter_factory::adapter_typ return factory_type::create(first, last); } +// The element type a container's data() points at, cv-qualifiers removed. +// Ill-formed - and therefore SFINAE-friendly - for types without data(). +template +using container_data_t = typename std::remove_cv().data()) >::type >::type; + +// The container's own element type, cv-qualifiers removed. It is looked up on +// the bare type so it is also found when ContainerType is deduced as a +// reference by the forwarding-reference overload below. +template +using container_value_t = typename std::remove_cv < + typename std::remove_cv::type>::type::value_type >::type; + // Detect a container that stores its elements contiguously as single bytes // (std::string, std::vector, std::array, // std::string_view, ...). Such inputs are wrapped in a pointer-based adapter so // they benefit from the contiguous fast paths (bulk string scanning, memcpy for // binary formats) in every C++ standard - not only in C++20, where the standard // library iterators model std::contiguous_iterator and are detected directly. +// +// data() and size() on their own would be duck typing: they say nothing about +// size() counting the units data() points at, and reading [data(), data() + +// size()) as bytes would be wrong for a type where it does not. Requiring the +// container's own value_type to be that same single-byte element ties the two +// together; every contiguous standard container satisfies it. Anything else +// keeps the iterator-based adapter, which is always correct - only slower. template struct is_contiguous_byte_container : std::false_type {}; template struct is_contiguous_byte_container < ContainerType, void_t < -decltype(std::declval().data()), + container_data_t, + container_value_t, decltype(std::declval().size()) >> : std::integral_constant < bool, std::is_pointer().data())>::value&& - std::is_integral().data())>::type>::value&& - sizeof(typename std::remove_pointer().data())>::type) == 1 > {}; + std::is_integral>::value&& + sizeof(container_data_t) == 1 && + std::is_same, container_value_t>::value > {}; // Convenience shorthand from container to iterator // Enables ADL on begin(container) and end(container) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 955add0ef..f5887ad73 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7623,23 +7623,45 @@ typename iterator_input_adapter_factory::adapter_typ return factory_type::create(first, last); } +// The element type a container's data() points at, cv-qualifiers removed. +// Ill-formed - and therefore SFINAE-friendly - for types without data(). +template +using container_data_t = typename std::remove_cv().data()) >::type >::type; + +// The container's own element type, cv-qualifiers removed. It is looked up on +// the bare type so it is also found when ContainerType is deduced as a +// reference by the forwarding-reference overload below. +template +using container_value_t = typename std::remove_cv < + typename std::remove_cv::type>::type::value_type >::type; + // Detect a container that stores its elements contiguously as single bytes // (std::string, std::vector, std::array, // std::string_view, ...). Such inputs are wrapped in a pointer-based adapter so // they benefit from the contiguous fast paths (bulk string scanning, memcpy for // binary formats) in every C++ standard - not only in C++20, where the standard // library iterators model std::contiguous_iterator and are detected directly. +// +// data() and size() on their own would be duck typing: they say nothing about +// size() counting the units data() points at, and reading [data(), data() + +// size()) as bytes would be wrong for a type where it does not. Requiring the +// container's own value_type to be that same single-byte element ties the two +// together; every contiguous standard container satisfies it. Anything else +// keeps the iterator-based adapter, which is always correct - only slower. template struct is_contiguous_byte_container : std::false_type {}; template struct is_contiguous_byte_container < ContainerType, void_t < -decltype(std::declval().data()), + container_data_t, + container_value_t, decltype(std::declval().size()) >> : std::integral_constant < bool, std::is_pointer().data())>::value&& - std::is_integral().data())>::type>::value&& - sizeof(typename std::remove_pointer().data())>::type) == 1 > {}; + std::is_integral>::value&& + sizeof(container_data_t) == 1 && + std::is_same, container_value_t>::value > {}; // Convenience shorthand from container to iterator // Enables ADL on begin(container) and end(container) diff --git a/tests/src/unit-user_defined_input.cpp b/tests/src/unit-user_defined_input.cpp index 63dc74531..ff219eaa9 100644 --- a/tests/src/unit-user_defined_input.cpp +++ b/tests/src/unit-user_defined_input.cpp @@ -18,6 +18,9 @@ #include using nlohmann::json; +#include // array +#include // size_t +#include // uint8_t #include #include // string #include // vector @@ -214,6 +217,65 @@ TEST_CASE("Parse with heterogeneous iterator and sentinel types") CHECK(j2.at(0) == 1); } +// A type whose data() hands out raw bytes but whose size() counts something +// else - here fixed-size records. Reading [data(), data() + size()) as bytes +// would silently truncate the input, so data() and size() alone must not be +// taken as evidence of contiguous byte storage. +struct record_buffer +{ + using value_type = std::array; + + std::string bytes; + + const char* data() const noexcept + { + return bytes.data(); + } + std::size_t size() const noexcept + { + return bytes.size() / sizeof(value_type); + } + const char* begin() const noexcept + { + return bytes.data(); + } + const char* end() const noexcept + { + return bytes.data() + bytes.size(); + } +}; + +TEST_CASE("Contiguous byte containers take the pointer adapter") +{ + // Containers with contiguous single-byte storage are routed through the + // pointer-based adapter so the bulk fast paths apply in every standard, not + // only in C++20 where the library iterators model std::contiguous_iterator. + CHECK(nlohmann::detail::is_contiguous_byte_container::value); + CHECK(nlohmann::detail::is_contiguous_byte_container>::value); + CHECK(nlohmann::detail::is_contiguous_byte_container>::value); + CHECK(nlohmann::detail::is_contiguous_byte_container>::value); + + // input_adapter() takes its container by forwarding reference, so the trait + // is also asked about reference types + CHECK(nlohmann::detail::is_contiguous_byte_container::value); + CHECK(nlohmann::detail::is_contiguous_byte_container::value); + + // everything else keeps the iterator-based adapter + CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container>::value); + CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container>::value); + CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container::value); + + // including a type that has data() and size() but whose size() does not + // count the units data() points at: its value_type says so + CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container::value); + + // and such a container still parses through its iterators, in full - taking + // it for a byte container would stop after data() + size() bytes + const record_buffer buffer{"[1,2,3,4,5]"}; + CHECK(buffer.size() * sizeof(record_buffer::value_type) < buffer.bytes.size()); + CHECK(json::parse(buffer) == json({1, 2, 3, 4, 5})); +} + #if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) // JSON_HAS_CPP_20 (do not remove; see note at top of file) TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t")