diff --git a/include/nlohmann/detail/input/input_adapters.hpp b/include/nlohmann/detail/input/input_adapters.hpp index 77a1b45c5..05fd4c816 100644 --- a/include/nlohmann/detail/input/input_adapters.hpp +++ b/include/nlohmann/detail/input/input_adapters.hpp @@ -8,17 +8,18 @@ #pragma once -#include // count, find, min +#include // count, find, max, min #include // array #include // size_t #include // uint32_t #include // strlen #include // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator #include // numeric_limits +#include // unique_ptr #include // streambuf #include // string, char_traits #include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer -#include // pair, declval +#include // declval, move, pair #include // vector #ifndef JSON_NO_IO @@ -72,6 +73,82 @@ enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; #ifndef JSON_NO_IO /*! +A growable array of characters for the part of a token that an input adapter +copied out of its window: unlike std::vector::push_back(), appending a +character takes no reference to a local, and is a few instructions when there +is room. +*/ +class token_carry_buffer +{ + public: + token_carry_buffer() = default; + token_carry_buffer(const token_carry_buffer&) = delete; + token_carry_buffer& operator=(const token_carry_buffer&) = delete; + token_carry_buffer(token_carry_buffer&& rhs) noexcept + : m_data(std::move(rhs.m_data)), m_end(rhs.m_end), m_capacity_end(rhs.m_capacity_end) + { + rhs.m_end = rhs.m_capacity_end = nullptr; + } + token_carry_buffer& operator=(token_carry_buffer&&) = delete; + ~token_carry_buffer() = default; + + void clear() noexcept + { + m_end = m_data.get(); + } + + void push_back(char c) + { + if (JSON_HEDLEY_UNLIKELY(m_end == m_capacity_end)) + { + grow(1); + } + *m_end++ = c; + } + + void append(const char* first, const char* last) + { + const auto n = static_cast(last - first); + const auto room = static_cast(m_capacity_end - m_end); + if (n > room) + { + grow(n); + } + if (n != 0) + { + std::memcpy(m_end, first, n); + } + m_end += n; + } + + /// replace the contents of @a out + void copy_to(std::vector& out) const + { + out.assign(m_data.get(), m_end); + } + + private: + /// make room for @a extra more characters + JSON_HEDLEY_NEVER_INLINE void grow(std::size_t extra) + { + const auto size = static_cast(m_end - m_data.get()); + const auto capacity = static_cast(m_capacity_end - m_data.get()); + const std::size_t new_capacity = (std::max)(capacity < 32 ? static_cast(32) : 2 * capacity, size + extra); + std::unique_ptr data(new char[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (size != 0) + { + std::memcpy(data.get(), m_data.get(), size); + } + m_data = std::move(data); + m_end = m_data.get() + size; + m_capacity_end = m_data.get() + new_capacity; + } + + std::unique_ptr m_data{}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + char* m_end = nullptr; + char* m_capacity_end = nullptr; +}; +/*! Input adapter for stdio file access. This adapter read only 1 byte and do not use any buffer. This adapter is a very low level adapter. */ @@ -209,6 +286,7 @@ class input_stream_adapter , token_segment(rhs.token_segment) , current_in_get_area(rhs.current_in_get_area) , tracking_token(rhs.tracking_token) + , no_get_area(rhs.no_get_area) #endif { rhs.is = nullptr; @@ -295,37 +373,20 @@ class input_stream_adapter std::char_traits::int_type get_character() { #if !JSON_DISABLE_STREAM_BULK_SCAN - // sbumpc() refills the get area if it is exhausted - const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); - if (JSON_HEDLEY_UNLIKELY(refill)) - { - save_token_segment(); - } -#endif - - auto res = sb->sbumpc(); - // set eof manually, as we don't use the istream interface. - if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) - { -#if !JSON_DISABLE_STREAM_BULK_SCAN - current_in_get_area = false; -#endif - is->clear(is->rdstate() | std::ios::eofbit); - } -#if !JSON_DISABLE_STREAM_BULK_SCAN - else if (JSON_HEDLEY_UNLIKELY(refill)) - { - // the consumed character is at gptr() - 1, unless the streambuf - // has no get area and returned it from uflow() - const char* const next = streambuf_access::get_ptr(sb); - after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); - } - else + // the character is in the get area: step over it, as sbumpc() does + const char* const next = streambuf_access::get_ptr(sb); + if (JSON_HEDLEY_LIKELY(next != streambuf_access::get_end(sb))) { current_in_get_area = true; + streambuf_access::bump(sb, 1); + return std::char_traits::to_int_type(*next); + } + if (no_get_area) + { + return get_character_no_get_area(); } #endif - return res; + return get_character_slow(); } #endif @@ -334,6 +395,18 @@ class input_stream_adapter { #if JSON_PRECISE_STREAM_POSITION commit_lookahead(); +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the elements are in the get area: copy them, as sgetn() does, but + // without the virtual call + const char* const first = streambuf_access::get_ptr(sb); + const auto available = static_cast(streambuf_access::get_end(sb) - first); + if (JSON_HEDLEY_LIKELY(count * sizeof(T) <= available && count * sizeof(T) <= static_cast((std::numeric_limits::max)()))) + { + std::memcpy(dest, first, count * sizeof(T)); + streambuf_access::bump(sb, static_cast(count * sizeof(T))); + return count * sizeof(T); + } #endif auto res = static_cast(sb->sgetn(reinterpret_cast(dest), static_cast(count * sizeof(T)))); if (JSON_HEDLEY_UNLIKELY(res < count * sizeof(T))) @@ -421,7 +494,7 @@ class input_stream_adapter // last one if @a drop_last. void copy_token(std::vector& out, bool drop_last) const { - out = token_carry; + token_carry.copy_to(out); if (token_segment != nullptr) { out.insert(out.end(), token_segment, current_end()); @@ -434,6 +507,47 @@ class input_stream_adapter #endif private: +#if !JSON_PRECISE_STREAM_POSITION + // the get area is exhausted, or there is none +#if !JSON_DISABLE_STREAM_BULK_SCAN + JSON_HEDLEY_NEVER_INLINE +#endif + std::char_traits::int_type get_character_slow() + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sbumpc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + + auto res = sb->sbumpc(); + // set eof manually, as we don't use the istream interface. + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif + is->clear(is->rdstate() | std::ios::eofbit); + } +#if !JSON_DISABLE_STREAM_BULK_SCAN + else if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the consumed character is at gptr() - 1, unless the streambuf + // has no get area and returned it from uflow() + const char* const next = streambuf_access::get_ptr(sb); + after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); + } + else + { + current_in_get_area = true; + } +#endif + return res; + } +#endif #if !JSON_DISABLE_STREAM_BULK_SCAN // Reads the protected get-area pointers of any std::streambuf. Naming the // members through a derived class makes forming the member pointers @@ -457,6 +571,11 @@ class input_stream_adapter { (s->*(&streambuf_access::gbump))(n); } + // sbumpc() for an exhausted get area + static std::streambuf::int_type next_character(std::streambuf* s) + { + return (s->*(&streambuf_access::uflow))(); + } }; // one past the character last returned by get_character(), if that is in @@ -477,16 +596,53 @@ class input_stream_adapter { if (token_segment != nullptr) { - token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb)); + token_carry.append(token_segment, streambuf_access::get_end(sb)); token_segment = nullptr; } } +#if !JSON_PRECISE_STREAM_POSITION + // The streambuf has no get area (e.g., std::cin synchronized with stdio): + // every character comes from uflow(), and the token is kept by value. + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area() + { + // the get area is exhausted, so this is what sbumpc() does + const auto res = streambuf_access::next_character(sb); + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof() || streambuf_access::get_end(sb) != nullptr)) + { + return get_character_no_get_area_other(res); + } + // current_in_get_area is false: only the first character of a get + // area, or the end of the input, is read here + if (tracking_token) + { + token_carry.push_back(std::char_traits::to_char_type(res)); + } + return res; + } + + // @a res came from uflow() of a streambuf without a get area: it is the + // end of the input, or the streambuf has a get area now + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area_other(std::char_traits::int_type res) + { + if (res == std::char_traits::eof()) + { + current_in_get_area = false; + is->clear(is->rdstate() | std::ios::eofbit); + return res; + } + // the consumed character is at gptr() - 1 + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_begin(sb)); + return res; + } +#endif + // character @a c was just read into a new get area (@a in_get_area) or // returned without one void after_refill(std::char_traits::int_type c, bool in_get_area) { current_in_get_area = in_get_area; + no_get_area = !in_get_area; if (!tracking_token) { return; @@ -525,13 +681,16 @@ class input_stream_adapter #endif #if !JSON_DISABLE_STREAM_BULK_SCAN /// the part of the current token that was in earlier get areas - std::vector token_carry{}; // NOLINT(readability-redundant-member-init) + token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init) /// start of the current token's part in the get area, or nullptr const char* token_segment = nullptr; /// whether the character last returned by get_character() is in the get area bool current_in_get_area = false; /// whether a token is tracked at all; the binary readers never start one bool tracking_token = false; + /// whether the streambuf had no get area when it was last asked for a + /// character + bool no_get_area = false; #endif }; #endif // JSON_NO_IO diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 89e89c12a..51661ab17 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -8468,17 +8468,18 @@ NLOHMANN_JSON_NAMESPACE_END -#include // count, find, min +#include // count, find, max, min #include // array #include // size_t #include // uint32_t #include // strlen #include // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator #include // numeric_limits +#include // unique_ptr #include // streambuf #include // string, char_traits #include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer -#include // pair, declval +#include // declval, move, pair #include // vector #ifndef JSON_NO_IO @@ -8537,6 +8538,82 @@ enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 }; #ifndef JSON_NO_IO /*! +A growable array of characters for the part of a token that an input adapter +copied out of its window: unlike std::vector::push_back(), appending a +character takes no reference to a local, and is a few instructions when there +is room. +*/ +class token_carry_buffer +{ + public: + token_carry_buffer() = default; + token_carry_buffer(const token_carry_buffer&) = delete; + token_carry_buffer& operator=(const token_carry_buffer&) = delete; + token_carry_buffer(token_carry_buffer&& rhs) noexcept + : m_data(std::move(rhs.m_data)), m_end(rhs.m_end), m_capacity_end(rhs.m_capacity_end) + { + rhs.m_end = rhs.m_capacity_end = nullptr; + } + token_carry_buffer& operator=(token_carry_buffer&&) = delete; + ~token_carry_buffer() = default; + + void clear() noexcept + { + m_end = m_data.get(); + } + + void push_back(char c) + { + if (JSON_HEDLEY_UNLIKELY(m_end == m_capacity_end)) + { + grow(1); + } + *m_end++ = c; + } + + void append(const char* first, const char* last) + { + const auto n = static_cast(last - first); + const auto room = static_cast(m_capacity_end - m_end); + if (n > room) + { + grow(n); + } + if (n != 0) + { + std::memcpy(m_end, first, n); + } + m_end += n; + } + + /// replace the contents of @a out + void copy_to(std::vector& out) const + { + out.assign(m_data.get(), m_end); + } + + private: + /// make room for @a extra more characters + JSON_HEDLEY_NEVER_INLINE void grow(std::size_t extra) + { + const auto size = static_cast(m_end - m_data.get()); + const auto capacity = static_cast(m_capacity_end - m_data.get()); + const std::size_t new_capacity = (std::max)(capacity < 32 ? static_cast(32) : 2 * capacity, size + extra); + std::unique_ptr data(new char[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + if (size != 0) + { + std::memcpy(data.get(), m_data.get(), size); + } + m_data = std::move(data); + m_end = m_data.get() + size; + m_capacity_end = m_data.get() + new_capacity; + } + + std::unique_ptr m_data{}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init) + char* m_end = nullptr; + char* m_capacity_end = nullptr; +}; +/*! Input adapter for stdio file access. This adapter read only 1 byte and do not use any buffer. This adapter is a very low level adapter. */ @@ -8674,6 +8751,7 @@ class input_stream_adapter , token_segment(rhs.token_segment) , current_in_get_area(rhs.current_in_get_area) , tracking_token(rhs.tracking_token) + , no_get_area(rhs.no_get_area) #endif { rhs.is = nullptr; @@ -8760,37 +8838,20 @@ class input_stream_adapter std::char_traits::int_type get_character() { #if !JSON_DISABLE_STREAM_BULK_SCAN - // sbumpc() refills the get area if it is exhausted - const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); - if (JSON_HEDLEY_UNLIKELY(refill)) - { - save_token_segment(); - } -#endif - - auto res = sb->sbumpc(); - // set eof manually, as we don't use the istream interface. - if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) - { -#if !JSON_DISABLE_STREAM_BULK_SCAN - current_in_get_area = false; -#endif - is->clear(is->rdstate() | std::ios::eofbit); - } -#if !JSON_DISABLE_STREAM_BULK_SCAN - else if (JSON_HEDLEY_UNLIKELY(refill)) - { - // the consumed character is at gptr() - 1, unless the streambuf - // has no get area and returned it from uflow() - const char* const next = streambuf_access::get_ptr(sb); - after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); - } - else + // the character is in the get area: step over it, as sbumpc() does + const char* const next = streambuf_access::get_ptr(sb); + if (JSON_HEDLEY_LIKELY(next != streambuf_access::get_end(sb))) { current_in_get_area = true; + streambuf_access::bump(sb, 1); + return std::char_traits::to_int_type(*next); + } + if (no_get_area) + { + return get_character_no_get_area(); } #endif - return res; + return get_character_slow(); } #endif @@ -8799,6 +8860,18 @@ class input_stream_adapter { #if JSON_PRECISE_STREAM_POSITION commit_lookahead(); +#endif +#if !JSON_DISABLE_STREAM_BULK_SCAN + // the elements are in the get area: copy them, as sgetn() does, but + // without the virtual call + const char* const first = streambuf_access::get_ptr(sb); + const auto available = static_cast(streambuf_access::get_end(sb) - first); + if (JSON_HEDLEY_LIKELY(count * sizeof(T) <= available && count * sizeof(T) <= static_cast((std::numeric_limits::max)()))) + { + std::memcpy(dest, first, count * sizeof(T)); + streambuf_access::bump(sb, static_cast(count * sizeof(T))); + return count * sizeof(T); + } #endif auto res = static_cast(sb->sgetn(reinterpret_cast(dest), static_cast(count * sizeof(T)))); if (JSON_HEDLEY_UNLIKELY(res < count * sizeof(T))) @@ -8886,7 +8959,7 @@ class input_stream_adapter // last one if @a drop_last. void copy_token(std::vector& out, bool drop_last) const { - out = token_carry; + token_carry.copy_to(out); if (token_segment != nullptr) { out.insert(out.end(), token_segment, current_end()); @@ -8899,6 +8972,47 @@ class input_stream_adapter #endif private: +#if !JSON_PRECISE_STREAM_POSITION + // the get area is exhausted, or there is none +#if !JSON_DISABLE_STREAM_BULK_SCAN + JSON_HEDLEY_NEVER_INLINE +#endif + std::char_traits::int_type get_character_slow() + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + // sbumpc() refills the get area if it is exhausted + const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb); + if (JSON_HEDLEY_UNLIKELY(refill)) + { + save_token_segment(); + } +#endif + + auto res = sb->sbumpc(); + // set eof manually, as we don't use the istream interface. + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) + { +#if !JSON_DISABLE_STREAM_BULK_SCAN + current_in_get_area = false; +#endif + is->clear(is->rdstate() | std::ios::eofbit); + } +#if !JSON_DISABLE_STREAM_BULK_SCAN + else if (JSON_HEDLEY_UNLIKELY(refill)) + { + // the consumed character is at gptr() - 1, unless the streambuf + // has no get area and returned it from uflow() + const char* const next = streambuf_access::get_ptr(sb); + after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb)); + } + else + { + current_in_get_area = true; + } +#endif + return res; + } +#endif #if !JSON_DISABLE_STREAM_BULK_SCAN // Reads the protected get-area pointers of any std::streambuf. Naming the // members through a derived class makes forming the member pointers @@ -8922,6 +9036,11 @@ class input_stream_adapter { (s->*(&streambuf_access::gbump))(n); } + // sbumpc() for an exhausted get area + static std::streambuf::int_type next_character(std::streambuf* s) + { + return (s->*(&streambuf_access::uflow))(); + } }; // one past the character last returned by get_character(), if that is in @@ -8942,16 +9061,53 @@ class input_stream_adapter { if (token_segment != nullptr) { - token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb)); + token_carry.append(token_segment, streambuf_access::get_end(sb)); token_segment = nullptr; } } +#if !JSON_PRECISE_STREAM_POSITION + // The streambuf has no get area (e.g., std::cin synchronized with stdio): + // every character comes from uflow(), and the token is kept by value. + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area() + { + // the get area is exhausted, so this is what sbumpc() does + const auto res = streambuf_access::next_character(sb); + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof() || streambuf_access::get_end(sb) != nullptr)) + { + return get_character_no_get_area_other(res); + } + // current_in_get_area is false: only the first character of a get + // area, or the end of the input, is read here + if (tracking_token) + { + token_carry.push_back(std::char_traits::to_char_type(res)); + } + return res; + } + + // @a res came from uflow() of a streambuf without a get area: it is the + // end of the input, or the streambuf has a get area now + JSON_HEDLEY_NEVER_INLINE std::char_traits::int_type get_character_no_get_area_other(std::char_traits::int_type res) + { + if (res == std::char_traits::eof()) + { + current_in_get_area = false; + is->clear(is->rdstate() | std::ios::eofbit); + return res; + } + // the consumed character is at gptr() - 1 + after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_begin(sb)); + return res; + } +#endif + // character @a c was just read into a new get area (@a in_get_area) or // returned without one void after_refill(std::char_traits::int_type c, bool in_get_area) { current_in_get_area = in_get_area; + no_get_area = !in_get_area; if (!tracking_token) { return; @@ -8990,13 +9146,16 @@ class input_stream_adapter #endif #if !JSON_DISABLE_STREAM_BULK_SCAN /// the part of the current token that was in earlier get areas - std::vector token_carry{}; // NOLINT(readability-redundant-member-init) + token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init) /// start of the current token's part in the get area, or nullptr const char* token_segment = nullptr; /// whether the character last returned by get_character() is in the get area bool current_in_get_area = false; /// whether a token is tracked at all; the binary readers never start one bool tracking_token = false; + /// whether the streambuf had no get area when it was last asked for a + /// character + bool no_get_area = false; #endif }; #endif // JSON_NO_IO diff --git a/tests/src/unit-stream-bulk-scan.cpp b/tests/src/unit-stream-bulk-scan.cpp index 14e61d6f5..d61ce4bd0 100644 --- a/tests/src/unit-stream-bulk-scan.cpp +++ b/tests/src/unit-stream-bulk-scan.cpp @@ -48,14 +48,16 @@ namespace class chunked_streambuf : public std::streambuf { public: - chunked_streambuf(std::string s, std::size_t chunk_size) - : m_data(std::move(s)), m_chunk_size(chunk_size) + // The first @a unbuffered characters are served without a get area, the + // rest in get areas of @a chunk_size. + chunked_streambuf(std::string s, std::size_t chunk_size, std::size_t unbuffered = 0) + : m_data(std::move(s)), m_chunk_size(chunk_size), m_unbuffered(unbuffered) {} protected: int_type underflow() override { - if (m_chunk_size == 0) + if (m_chunk_size == 0 || m_pos < m_unbuffered) { return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof(); } @@ -77,7 +79,7 @@ class chunked_streambuf : public std::streambuf int_type uflow() override { - if (m_chunk_size == 0) + if (m_chunk_size == 0 || m_pos < m_unbuffered) { return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof(); } @@ -88,6 +90,7 @@ class chunked_streambuf : public std::streambuf std::string m_data; std::size_t m_pos = 0; std::size_t m_chunk_size; + std::size_t m_unbuffered; }; // how a test reads from the stream @@ -101,9 +104,9 @@ enum class read_mode // Read @a input from a chunked_streambuf and describe everything observable: // the values or the exception, the stream state after each value, and the // characters left in the stream. -std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode) +std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode, std::size_t unbuffered = 0) { - chunked_streambuf buf(input, chunk_size); + chunked_streambuf buf(input, chunk_size, unbuffered); std::istream is(&buf); std::string result; for (int i = 0; i < 4; ++i) @@ -212,6 +215,31 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk") } } + SECTION("results do not depend on a get area that appears late") + { + // the first characters come without a get area, then the streambuf + // starts to use one: the token that is read at that point is complete + for (const auto& input : corpus()) + { + const std::string expected = observe(input, 0, read_mode::extract); + for (const std::size_t unbuffered : + { + 1u, 2u, 3u, 5u, 8u, 13u, 21u + }) + { + for (const std::size_t chunk_size : + { + 1u, 2u, 3u, 7u, 64u + }) + { + CAPTURE(input) + CAPTURE(unbuffered) + CAPTURE(chunk_size) + CHECK(observe(input, chunk_size, read_mode::extract, unbuffered) == expected); + } + } + } + } #endif // !defined(JSON_NOEXCEPTION) SECTION("values are the same as when parsing from a string")