mirror of
https://github.com/nlohmann/json.git
synced 2026-08-28 20:07:32 +00:00
Require a container's value_type to match what data() points at
is_contiguous_byte_container accepted any type with a data() returning a
pointer to a single-byte integral plus a size(). That is duck typing: the two
members say nothing about size() counting the units data() points at.
A type where it does not - fixed-size records, say - was routed to the
pointer-based adapter and parsed as [data(), data() + size()) bytes, silently
truncating input the iterator-based adapter had read in full:
struct record_buffer {
using value_type = std::array<char, 4>;
std::string bytes;
const char* data() const; // raw bytes
std::size_t size() const; // in records
const char* begin() const; const char* end() const;
};
json::parse(record_buffer{"[1,2,3,4,5]"}); // parse error at column 3
Requiring the container's own value_type to be that same element type ties the
two together. Every contiguous standard container satisfies it, so std::string,
std::vector<char>, std::array<char, N> and std::string_view keep the fast path;
anything else falls back to the iterator-based adapter, which is always correct.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXDi7NTMKAmoArUZSKMc4T
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -620,23 +620,45 @@ typename iterator_input_adapter_factory<IteratorType, SentinelType>::adapter_typ
|
|||||||
return factory_type::create(first, last);
|
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<typename ContainerType>
|
||||||
|
using container_data_t = typename std::remove_cv<typename std::remove_pointer <
|
||||||
|
decltype(std::declval<const ContainerType&>().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<typename ContainerType>
|
||||||
|
using container_value_t = typename std::remove_cv <
|
||||||
|
typename std::remove_cv<typename std::remove_reference<ContainerType>::type>::type::value_type >::type;
|
||||||
|
|
||||||
// Detect a container that stores its elements contiguously as single bytes
|
// Detect a container that stores its elements contiguously as single bytes
|
||||||
// (std::string, std::vector<char/unsigned char>, std::array<char, N>,
|
// (std::string, std::vector<char/unsigned char>, std::array<char, N>,
|
||||||
// std::string_view, ...). Such inputs are wrapped in a pointer-based adapter so
|
// 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
|
// 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
|
// binary formats) in every C++ standard - not only in C++20, where the standard
|
||||||
// library iterators model std::contiguous_iterator and are detected directly.
|
// 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<typename ContainerType, typename = void>
|
template<typename ContainerType, typename = void>
|
||||||
struct is_contiguous_byte_container : std::false_type {};
|
struct is_contiguous_byte_container : std::false_type {};
|
||||||
|
|
||||||
template<typename ContainerType>
|
template<typename ContainerType>
|
||||||
struct is_contiguous_byte_container < ContainerType, void_t <
|
struct is_contiguous_byte_container < ContainerType, void_t <
|
||||||
decltype(std::declval<const ContainerType&>().data()),
|
container_data_t<ContainerType>,
|
||||||
|
container_value_t<ContainerType>,
|
||||||
decltype(std::declval<const ContainerType&>().size()) >>
|
decltype(std::declval<const ContainerType&>().size()) >>
|
||||||
: std::integral_constant < bool,
|
: std::integral_constant < bool,
|
||||||
std::is_pointer<decltype(std::declval<const ContainerType&>().data())>::value&&
|
std::is_pointer<decltype(std::declval<const ContainerType&>().data())>::value&&
|
||||||
std::is_integral<typename std::remove_pointer<decltype(std::declval<const ContainerType&>().data())>::type>::value&&
|
std::is_integral<container_data_t<ContainerType>>::value&&
|
||||||
sizeof(typename std::remove_pointer<decltype(std::declval<const ContainerType&>().data())>::type) == 1 > {};
|
sizeof(container_data_t<ContainerType>) == 1 &&
|
||||||
|
std::is_same<container_data_t<ContainerType>, container_value_t<ContainerType>>::value > {};
|
||||||
|
|
||||||
// Convenience shorthand from container to iterator
|
// Convenience shorthand from container to iterator
|
||||||
// Enables ADL on begin(container) and end(container)
|
// Enables ADL on begin(container) and end(container)
|
||||||
|
|||||||
@@ -7623,23 +7623,45 @@ typename iterator_input_adapter_factory<IteratorType, SentinelType>::adapter_typ
|
|||||||
return factory_type::create(first, last);
|
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<typename ContainerType>
|
||||||
|
using container_data_t = typename std::remove_cv<typename std::remove_pointer <
|
||||||
|
decltype(std::declval<const ContainerType&>().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<typename ContainerType>
|
||||||
|
using container_value_t = typename std::remove_cv <
|
||||||
|
typename std::remove_cv<typename std::remove_reference<ContainerType>::type>::type::value_type >::type;
|
||||||
|
|
||||||
// Detect a container that stores its elements contiguously as single bytes
|
// Detect a container that stores its elements contiguously as single bytes
|
||||||
// (std::string, std::vector<char/unsigned char>, std::array<char, N>,
|
// (std::string, std::vector<char/unsigned char>, std::array<char, N>,
|
||||||
// std::string_view, ...). Such inputs are wrapped in a pointer-based adapter so
|
// 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
|
// 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
|
// binary formats) in every C++ standard - not only in C++20, where the standard
|
||||||
// library iterators model std::contiguous_iterator and are detected directly.
|
// 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<typename ContainerType, typename = void>
|
template<typename ContainerType, typename = void>
|
||||||
struct is_contiguous_byte_container : std::false_type {};
|
struct is_contiguous_byte_container : std::false_type {};
|
||||||
|
|
||||||
template<typename ContainerType>
|
template<typename ContainerType>
|
||||||
struct is_contiguous_byte_container < ContainerType, void_t <
|
struct is_contiguous_byte_container < ContainerType, void_t <
|
||||||
decltype(std::declval<const ContainerType&>().data()),
|
container_data_t<ContainerType>,
|
||||||
|
container_value_t<ContainerType>,
|
||||||
decltype(std::declval<const ContainerType&>().size()) >>
|
decltype(std::declval<const ContainerType&>().size()) >>
|
||||||
: std::integral_constant < bool,
|
: std::integral_constant < bool,
|
||||||
std::is_pointer<decltype(std::declval<const ContainerType&>().data())>::value&&
|
std::is_pointer<decltype(std::declval<const ContainerType&>().data())>::value&&
|
||||||
std::is_integral<typename std::remove_pointer<decltype(std::declval<const ContainerType&>().data())>::type>::value&&
|
std::is_integral<container_data_t<ContainerType>>::value&&
|
||||||
sizeof(typename std::remove_pointer<decltype(std::declval<const ContainerType&>().data())>::type) == 1 > {};
|
sizeof(container_data_t<ContainerType>) == 1 &&
|
||||||
|
std::is_same<container_data_t<ContainerType>, container_value_t<ContainerType>>::value > {};
|
||||||
|
|
||||||
// Convenience shorthand from container to iterator
|
// Convenience shorthand from container to iterator
|
||||||
// Enables ADL on begin(container) and end(container)
|
// Enables ADL on begin(container) and end(container)
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
|
|
||||||
|
#include <array> // array
|
||||||
|
#include <cstddef> // size_t
|
||||||
|
#include <cstdint> // uint8_t
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <string> // string
|
#include <string> // string
|
||||||
#include <vector> // vector
|
#include <vector> // vector
|
||||||
@@ -214,6 +217,65 @@ TEST_CASE("Parse with heterogeneous iterator and sentinel types")
|
|||||||
CHECK(j2.at(0) == 1);
|
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<char, 4>;
|
||||||
|
|
||||||
|
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<std::string>::value);
|
||||||
|
CHECK(nlohmann::detail::is_contiguous_byte_container<std::vector<char>>::value);
|
||||||
|
CHECK(nlohmann::detail::is_contiguous_byte_container<std::vector<std::uint8_t>>::value);
|
||||||
|
CHECK(nlohmann::detail::is_contiguous_byte_container<std::array<char, 4>>::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<std::string&>::value);
|
||||||
|
CHECK(nlohmann::detail::is_contiguous_byte_container<const std::string&>::value);
|
||||||
|
|
||||||
|
// everything else keeps the iterator-based adapter
|
||||||
|
CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<std::list<char>>::value);
|
||||||
|
CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<std::vector<int>>::value);
|
||||||
|
CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<const char*>::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<record_buffer>::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)
|
#if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20)
|
||||||
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
|
// 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")
|
TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t")
|
||||||
|
|||||||
Reference in New Issue
Block a user