From 106065e62d27db400a1d6ac3c491efe3bc75a707 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 15 Oct 2026 08:17:31 +0200 Subject: [PATCH] Make the value lifecycle inline-friendly Signed-off-by: Niels Lohmann --- include/nlohmann/json.hpp | 198 +++++++++++++++++++++++-------- single_include/nlohmann/json.hpp | 198 +++++++++++++++++++++++-------- tests/src/unit-comparison.cpp | 155 ++++++++++++++++++++++++ tests/src/unit-constructor2.cpp | 139 ++++++++++++++++++++++ 4 files changed, 586 insertions(+), 104 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index aed3938af..e4cc3d6df 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -585,26 +585,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (t) { case value_t::object: - { - object = create(); - break; - } - case value_t::array: - { - array = create(); - break; - } - case value_t::string: - { - string = create(""); - break; - } - case value_t::binary: { - binary = create(); + // allocating: kept out of line, so the scalar cases below inline + init_owned(t); break; } @@ -651,6 +637,34 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } } + JSON_HEDLEY_NEVER_INLINE + void init_owned(value_t t) + { + switch (t) + { + case value_t::object: + object = create(); + break; + case value_t::array: + array = create(); + break; + case value_t::string: + string = create(""); + break; + case value_t::binary: + binary = create(); + break; + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + break; + } + } + /// constructor for strings json_value(const string_t& value) : string(create(value)) {} @@ -911,7 +925,18 @@ public: } } + // only strings, binaries, arrays and objects own memory; keep the + // check for them inline so destroying a scalar is a compare and a + // branch, and leave the actual work to an out-of-line function void destroy(value_t t) + { + if (JSON_HEDLEY_UNLIKELY(t == value_t::string || t == value_t::binary || t == value_t::object || t == value_t::array)) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { @@ -2559,15 +2584,15 @@ public: // check of passed value is valid other.assert_invariant(); - if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + if (JSON_HEDLEY_LIKELY(m_data.m_type != value_t::object && m_data.m_type != value_t::array + && m_data.m_type != value_t::string && m_data.m_type != value_t::binary)) { - // copying the container directly would call this constructor again - // for every element, once per nesting level - copy_structured(other); + // null, boolean, numbers, discarded: plain copy of the union + m_data.m_value = other.m_data.m_value; } else { - copy_leaf_value(other, *this); + copy_owning(other); } assert_invariant(); @@ -2799,6 +2824,22 @@ public: // value access // ////////////////// + /// out-of-line part of the copy constructor for owning types + JSON_HEDLEY_NEVER_INLINE + void copy_owning(const basic_json& other) + { + if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + { + // copying the container directly would call this constructor again + // for every element, once per nesting level + copy_structured(other); + } + else + { + copy_leaf_value(other, *this); + } + } + /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -5173,6 +5214,20 @@ public: /// @name lexicographical comparison operators /// @{ + // the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR + // and JSON_IMPLEMENT_SCALAR_FAST +#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::null: \ + return (null_result); \ + case value_t::boolean: \ + return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ + case value_t::number_integer: \ + return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ + case value_t::number_unsigned: \ + return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ + case value_t::number_float: \ + return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); + // note parentheses around operands are necessary; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value @@ -5209,24 +5264,11 @@ public: return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ } \ \ - case value_t::null: \ - return (null_result); \ - \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + \ case value_t::string: \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ \ - case value_t::boolean: \ - return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ - \ - case value_t::number_integer: \ - return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ - \ - case value_t::number_unsigned: \ - return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ - \ - case value_t::number_float: \ - return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \ - \ case value_t::binary: \ return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \ \ @@ -5270,6 +5312,24 @@ public: \ return (default_result); + // inline fast path for two values of the same non-owning scalar type; anything + // else falls through to the out-of-line general comparison +#define JSON_IMPLEMENT_SCALAR_FAST(op, null_result) \ + if (lhs.m_data.m_type == rhs.m_data.m_type) \ + { \ + switch (lhs.m_data.m_type) \ + { \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::object: \ + case value_t::array: \ + case value_t::string: \ + case value_t::binary: \ + case value_t::discarded: \ + default: \ + break; \ + } \ + } + JSON_PRIVATE_UNLESS_TESTED: // returns true if: // - any operand is NaN and the other operand is of number type @@ -5297,6 +5357,46 @@ public: return compares_unordered(*this, rhs, inverse); } + // out-of-line general comparisons; the operators below handle same-type + // scalars inline and fall through to these + JSON_HEDLEY_NEVER_INLINE + static bool equal_general(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + JSON_HEDLEY_NEVER_INLINE + static bool less_general(const_reference lhs, const_reference rhs) noexcept + { + // default_result is used if we cannot compare values. In that case, + // we compare types. Note we have to call the operator explicitly, + // because MSVC has problems otherwise. + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) + } + +#if JSON_HAS_THREE_WAY_COMPARISON + JSON_HEDLEY_NEVER_INLINE + static std::partial_ordering three_way_general(const_reference lhs, const_reference rhs) noexcept // *NOPAD* + { + // default_result is used if we cannot compare values. In that case, + // we compare types. + JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* + std::partial_ordering::equivalent, + std::partial_ordering::unordered, + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + } +#endif + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -5308,11 +5408,11 @@ public: JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_general(lhs, rhs); } /// @brief comparison: equal @@ -5329,13 +5429,8 @@ public: std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* { const_reference lhs = *this; - // default_result is used if we cannot compare values. In that case, - // we compare types. - JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* - std::partial_ordering::equivalent, - std::partial_ordering::unordered, - lhs_type <=> rhs_type, // *NOPAD* - to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + JSON_IMPLEMENT_SCALAR_FAST(<=>, std::partial_ordering::equivalent) // *NOPAD* + return three_way_general(lhs, rhs); } /// @brief comparison: 3-way @@ -5423,11 +5518,11 @@ public: JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_general(lhs, rhs); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -5482,11 +5577,8 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ friend bool operator<(const_reference lhs, const_reference rhs) noexcept { - // default_result is used if we cannot compare values. In that case, - // we compare types. Note we have to call the operator explicitly, - // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), - compare_iteratively(lhs, rhs, true) == compare_result::less, false) + JSON_IMPLEMENT_SCALAR_FAST( <, false) + return less_general(lhs, rhs); } /// @brief comparison: less than @@ -5597,6 +5689,8 @@ public: #endif #undef JSON_IMPLEMENT_OPERATOR +#undef JSON_IMPLEMENT_SCALAR_FAST +#undef JSON_IMPLEMENT_SCALAR_CASES /// @} diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2f4fa38a2..0c2762a18 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -30325,26 +30325,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (t) { case value_t::object: - { - object = create(); - break; - } - case value_t::array: - { - array = create(); - break; - } - case value_t::string: - { - string = create(""); - break; - } - case value_t::binary: { - binary = create(); + // allocating: kept out of line, so the scalar cases below inline + init_owned(t); break; } @@ -30391,6 +30377,34 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } } + JSON_HEDLEY_NEVER_INLINE + void init_owned(value_t t) + { + switch (t) + { + case value_t::object: + object = create(); + break; + case value_t::array: + array = create(); + break; + case value_t::string: + string = create(""); + break; + case value_t::binary: + binary = create(); + break; + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + break; + } + } + /// constructor for strings json_value(const string_t& value) : string(create(value)) {} @@ -30651,7 +30665,18 @@ public: } } + // only strings, binaries, arrays and objects own memory; keep the + // check for them inline so destroying a scalar is a compare and a + // branch, and leave the actual work to an out-of-line function void destroy(value_t t) + { + if (JSON_HEDLEY_UNLIKELY(t == value_t::string || t == value_t::binary || t == value_t::object || t == value_t::array)) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { @@ -32299,15 +32324,15 @@ public: // check of passed value is valid other.assert_invariant(); - if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + if (JSON_HEDLEY_LIKELY(m_data.m_type != value_t::object && m_data.m_type != value_t::array + && m_data.m_type != value_t::string && m_data.m_type != value_t::binary)) { - // copying the container directly would call this constructor again - // for every element, once per nesting level - copy_structured(other); + // null, boolean, numbers, discarded: plain copy of the union + m_data.m_value = other.m_data.m_value; } else { - copy_leaf_value(other, *this); + copy_owning(other); } assert_invariant(); @@ -32539,6 +32564,22 @@ public: // value access // ////////////////// + /// out-of-line part of the copy constructor for owning types + JSON_HEDLEY_NEVER_INLINE + void copy_owning(const basic_json& other) + { + if (m_data.m_type == value_t::object || m_data.m_type == value_t::array) + { + // copying the container directly would call this constructor again + // for every element, once per nesting level + copy_structured(other); + } + else + { + copy_leaf_value(other, *this); + } + } + /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { @@ -34913,6 +34954,20 @@ public: /// @name lexicographical comparison operators /// @{ + // the same-type cases for non-owning scalars, shared by JSON_IMPLEMENT_OPERATOR + // and JSON_IMPLEMENT_SCALAR_FAST +#define JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::null: \ + return (null_result); \ + case value_t::boolean: \ + return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ + case value_t::number_integer: \ + return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ + case value_t::number_unsigned: \ + return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ + case value_t::number_float: \ + return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); + // note parentheses around operands are necessary; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value @@ -34949,24 +35004,11 @@ public: return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ } \ \ - case value_t::null: \ - return (null_result); \ - \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + \ case value_t::string: \ return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ \ - case value_t::boolean: \ - return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ - \ - case value_t::number_integer: \ - return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ - \ - case value_t::number_unsigned: \ - return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ - \ - case value_t::number_float: \ - return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \ - \ case value_t::binary: \ return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \ \ @@ -35010,6 +35052,24 @@ public: \ return (default_result); + // inline fast path for two values of the same non-owning scalar type; anything + // else falls through to the out-of-line general comparison +#define JSON_IMPLEMENT_SCALAR_FAST(op, null_result) \ + if (lhs.m_data.m_type == rhs.m_data.m_type) \ + { \ + switch (lhs.m_data.m_type) \ + { \ + JSON_IMPLEMENT_SCALAR_CASES(op, null_result) \ + case value_t::object: \ + case value_t::array: \ + case value_t::string: \ + case value_t::binary: \ + case value_t::discarded: \ + default: \ + break; \ + } \ + } + JSON_PRIVATE_UNLESS_TESTED: // returns true if: // - any operand is NaN and the other operand is of number type @@ -35037,6 +35097,46 @@ public: return compares_unordered(*this, rhs, inverse); } + // out-of-line general comparisons; the operators below handle same-type + // scalars inline and fall through to these + JSON_HEDLEY_NEVER_INLINE + static bool equal_general(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + JSON_IMPLEMENT_OPERATOR( ==, true, false, false, + compare_iteratively(lhs, rhs, false) == compare_result::equal, true) +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + } + + JSON_HEDLEY_NEVER_INLINE + static bool less_general(const_reference lhs, const_reference rhs) noexcept + { + // default_result is used if we cannot compare values. In that case, + // we compare types. Note we have to call the operator explicitly, + // because MSVC has problems otherwise. + JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), + compare_iteratively(lhs, rhs, true) == compare_result::less, false) + } + +#if JSON_HAS_THREE_WAY_COMPARISON + JSON_HEDLEY_NEVER_INLINE + static std::partial_ordering three_way_general(const_reference lhs, const_reference rhs) noexcept // *NOPAD* + { + // default_result is used if we cannot compare values. In that case, + // we compare types. + JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* + std::partial_ordering::equivalent, + std::partial_ordering::unordered, + lhs_type <=> rhs_type, // *NOPAD* + to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + } +#endif + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -35048,11 +35148,11 @@ public: JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif const_reference lhs = *this; - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_general(lhs, rhs); } /// @brief comparison: equal @@ -35069,13 +35169,8 @@ public: std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* { const_reference lhs = *this; - // default_result is used if we cannot compare values. In that case, - // we compare types. - JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD* - std::partial_ordering::equivalent, - std::partial_ordering::unordered, - lhs_type <=> rhs_type, // *NOPAD* - to_partial_ordering(compare_iteratively(lhs, rhs, false)), true) + JSON_IMPLEMENT_SCALAR_FAST(<=>, std::partial_ordering::equivalent) // *NOPAD* + return three_way_general(lhs, rhs); } /// @brief comparison: 3-way @@ -35163,11 +35258,11 @@ public: JSON_HEDLEY_DIAGNOSTIC_PUSH JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") #endif - JSON_IMPLEMENT_OPERATOR( ==, true, false, false, - compare_iteratively(lhs, rhs, false) == compare_result::equal, true) + JSON_IMPLEMENT_SCALAR_FAST( ==, true) #ifdef __GNUC__ JSON_HEDLEY_DIAGNOSTIC_POP #endif + return equal_general(lhs, rhs); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -35222,11 +35317,8 @@ public: /// @sa https://json.nlohmann.me/api/basic_json/operator_lt/ friend bool operator<(const_reference lhs, const_reference rhs) noexcept { - // default_result is used if we cannot compare values. In that case, - // we compare types. Note we have to call the operator explicitly, - // because MSVC has problems otherwise. - JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type), - compare_iteratively(lhs, rhs, true) == compare_result::less, false) + JSON_IMPLEMENT_SCALAR_FAST( <, false) + return less_general(lhs, rhs); } /// @brief comparison: less than @@ -35337,6 +35429,8 @@ public: #endif #undef JSON_IMPLEMENT_OPERATOR +#undef JSON_IMPLEMENT_SCALAR_FAST +#undef JSON_IMPLEMENT_SCALAR_CASES /// @} diff --git a/tests/src/unit-comparison.cpp b/tests/src/unit-comparison.cpp index 1e409bfb0..4ac43320b 100644 --- a/tests/src/unit-comparison.cpp +++ b/tests/src/unit-comparison.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -1149,3 +1150,157 @@ TEST_CASE("operator<=> of binary values with a different subtype does not depend } } #endif + +TEST_CASE("comparison of scalars (inline fast path)") +{ + const json::number_integer_t neg = -1; + const json::number_integer_t pos = 5; + const json::number_unsigned_t big = (std::numeric_limits::max)(); + + SECTION("same type") + { + CHECK(json(pos) == json(pos)); + CHECK(json(pos) != json(neg)); + CHECK(json(neg) < json(pos)); + CHECK_FALSE(json(pos) < json(neg)); + CHECK_FALSE(json(pos) < json(pos)); + CHECK(json(pos) <= json(pos)); + + CHECK(json(1u) == json(1u)); + CHECK(json(1u) < json(2u)); + CHECK_FALSE(json(2u) < json(1u)); + + CHECK(json(1.5) == json(1.5)); + CHECK(json(1.5) < json(2.5)); + CHECK_FALSE(json(2.5) < json(1.5)); + CHECK(json(-0.0) == json(0.0)); + } + + SECTION("mixed number types") + { + // negative signed vs unsigned must not wrap around + CHECK(json(neg) < json(1u)); + CHECK(json(1u) > json(neg)); + CHECK(json(neg) != json(big)); + CHECK(json(neg) < json(big)); + CHECK_FALSE(json(big) < json(neg)); + + CHECK(json(pos) == json(5u)); + CHECK(json(5u) == json(pos)); + CHECK(json(pos) < json(6u)); + CHECK(json(pos) == json(5.0)); + CHECK(json(5.0) == json(pos)); + CHECK(json(5u) == json(5.0)); + CHECK(json(neg) < json(0.5)); + CHECK(json(0.5) < json(1u)); + } + + SECTION("boolean and null") + { + CHECK(json(true) == json(true)); + CHECK(json(true) != json(false)); + CHECK(json(false) < json(true)); + CHECK_FALSE(json(true) < json(false)); + CHECK_FALSE(json(true) < json(true)); + + CHECK(json(nullptr) == json(nullptr)); + CHECK_FALSE(json(nullptr) < json(nullptr)); + CHECK(json(nullptr) <= json(nullptr)); + CHECK(json(nullptr) < json(false)); + CHECK(json(nullptr) != json(false)); + CHECK(json(nullptr) != json(0)); + } + + SECTION("string vs number") + { + CHECK(json("5") != json(5)); + CHECK(json(5) != json("5")); + CHECK(json(5) < json("5")); // number < string by type order + CHECK_FALSE(json("5") < json(5)); + CHECK(json(1.5) < json("")); + CHECK(json("a") == json("a")); + CHECK(json("a") < json("b")); + } + + SECTION("NaN") + { + const json nan = std::numeric_limits::quiet_NaN(); + const json one = 1.0; + + CHECK_FALSE(nan == nan); + CHECK(nan != nan); + CHECK_FALSE(nan < nan); + CHECK_FALSE(nan <= nan); + CHECK_FALSE(nan >= nan); + CHECK_FALSE(nan > nan); + + CHECK_FALSE(nan == one); + CHECK_FALSE(one == nan); + CHECK_FALSE(nan < one); + CHECK_FALSE(one < nan); + CHECK_FALSE(nan <= one); + CHECK_FALSE(one <= nan); + CHECK_FALSE(nan >= one); + CHECK_FALSE(one >= nan); + + // NaN against other number types + CHECK_FALSE(nan == json(1)); + CHECK_FALSE(json(1) == nan); + CHECK_FALSE(nan < json(1u)); + CHECK_FALSE(json(1u) < nan); + +#if JSON_HAS_THREE_WAY_COMPARISON + CHECK((nan <=> nan) == std::partial_ordering::unordered); // *NOPAD* + CHECK((nan <=> one) == std::partial_ordering::unordered); // *NOPAD* + CHECK((one <=> nan) == std::partial_ordering::unordered); // *NOPAD* + CHECK((one <=> json(2.0)) == std::partial_ordering::less); // *NOPAD* + CHECK((json(2.0) <=> one) == std::partial_ordering::greater); // *NOPAD* + CHECK((one <=> one) == std::partial_ordering::equivalent); // *NOPAD* + CHECK((json(neg) <=> json(1u)) == std::partial_ordering::less); // *NOPAD* + CHECK((json(nullptr) <=> json(nullptr)) == std::partial_ordering::equivalent); // *NOPAD* + CHECK((json(true) <=> json(false)) == std::partial_ordering::greater); // *NOPAD* +#endif + } + + SECTION("discarded") + { + const json d(json::value_t::discarded); + + CHECK_FALSE(d == d); + CHECK_FALSE(d < d); + CHECK_FALSE(d == json(1)); + CHECK_FALSE(json(1) == d); + CHECK_FALSE(d < json(1)); + CHECK_FALSE(json(1) < d); + CHECK_FALSE(d == json(nullptr)); + CHECK_FALSE(json(nullptr) == d); + +#if JSON_HAS_THREE_WAY_COMPARISON + CHECK((d <=> d) == std::partial_ordering::unordered); // *NOPAD* + CHECK((d <=> json(1)) == std::partial_ordering::unordered); // *NOPAD* + CHECK((json(1) <=> d) == std::partial_ordering::unordered); // *NOPAD* +#endif + } + + SECTION("scalar overloads") + { + CHECK(json(5) == 5); + CHECK(5 == json(5)); + CHECK(json(5) < 6); + CHECK(4 < json(5)); + CHECK(json(1.5) == 1.5); + CHECK(json(true) == true); + CHECK(json(5) != 6); + CHECK(json(1u) == 1.0); + } + + SECTION("structured values still compare") + { + CHECK(json({1, 2}) == json({1, 2})); + CHECK(json({1, 2}) < json({1, 3})); + CHECK(json({{"a", 1}}) == json({{"a", 1}})); + CHECK(json::binary({1, 2}) == json::binary({1, 2})); + CHECK(json(1) < json::array()); + CHECK(json::object() < json::array()); + } +} diff --git a/tests/src/unit-constructor2.cpp b/tests/src/unit-constructor2.cpp index d1ff56452..1334d6245 100644 --- a/tests/src/unit-constructor2.cpp +++ b/tests/src/unit-constructor2.cpp @@ -8,6 +8,9 @@ #include "doctest_compatibility.h" +#include +#include + #include using nlohmann::json; @@ -184,3 +187,139 @@ TEST_CASE("other constructors and destructor") } } } + +TEST_CASE("copy constructor and destructor across value types") +{ + SECTION("copy of a scalar is independent of the source") + { + const json src_null = nullptr; + const json src_bool = true; + const json src_int = -7; + const json src_uint = 7u; + const json src_float = 2.5; + + json c_null(src_null); + json c_bool(src_bool); + json c_int(src_int); + json c_uint(src_uint); + json c_float(src_float); + + CHECK(c_null.is_null()); + CHECK(c_bool == true); + CHECK(c_int == -7); + CHECK(c_uint == 7u); + CHECK(c_float == 2.5); + + c_int = 8; + c_float = 3.5; + CHECK(src_int == -7); + CHECK(src_float == 2.5); + } + + SECTION("copy of an owning value, then mutate the copy") + { + const json src_string = "hello"; + const json src_array = {1, 2, 3}; + const json src_object = {{"a", 1}, {"b", {{"c", 2}}}}; + const json src_binary = json::binary({1, 2, 3}); + + json c_string(src_string); + json c_array(src_array); + json c_object(src_object); + json c_binary(src_binary); + + CHECK(c_string == src_string); + CHECK(c_array == src_array); + CHECK(c_object == src_object); + CHECK(c_binary == src_binary); + + c_string.get_ref() += " world"; + c_array.push_back(4); + c_object["b"]["c"] = 3; + c_object["d"] = 4; + c_binary.get_binary().push_back(4); + + CHECK(src_string == "hello"); + CHECK(src_array.size() == 3); + CHECK(src_object["b"]["c"] == 2); + CHECK_FALSE(src_object.contains("d")); + CHECK(src_binary.get_binary().size() == 3); + + CHECK(c_string == "hello world"); + CHECK(c_array.size() == 4); + CHECK(c_object["b"]["c"] == 3); + CHECK(c_binary.get_binary().size() == 4); + } + + SECTION("copy keeps parent pointers valid (JSON_DIAGNOSTICS)") + { + const json src = {{"a", {1, 2}}, {"b", "x"}}; + json c(src); + c["a"].push_back(3); + CHECK(c["a"].size() == 3); + CHECK(src["a"].size() == 2); + } + + SECTION("vector of mixed types: build, copy, and destroy") + { + { + std::vector v; + for (int i = 0; i < 70; ++i) + { + switch (i % 9) + { + case 0: + v.emplace_back(nullptr); + break; + case 1: + v.emplace_back(i % 2 == 0); + break; + case 2: + v.emplace_back(-i); + break; + case 3: + v.emplace_back(static_cast(i)); + break; + case 4: + v.emplace_back(i * 0.5); + break; + case 5: + v.emplace_back(std::string(static_cast(i), 'x')); + break; + case 6: + v.emplace_back(json::array({i, "s", nullptr})); + break; + case 7: + v.emplace_back(json::object({{"k", i}})); + break; + default: + v.emplace_back(json::binary({1, 2, 3})); + break; + } + } + REQUIRE(v.size() == 70); + + std::vector copy(v); + CHECK(copy == v); + + // mutating the copy leaves the original alone + copy[5] = "changed"; + copy[6].push_back(1); + CHECK(copy != v); + CHECK(v[5] == std::string(5, 'x')); + CHECK(v[6].size() == 3); + + v.erase(v.begin(), v.begin() + 10); + CHECK(v.size() == 60); + } + + // destroying the discarded and default-constructed values + std::vector w(4); + w.emplace_back(json::value_t::discarded); + w.emplace_back(json::value_t::object); + w.emplace_back(json::value_t::array); + w.emplace_back(json::value_t::string); + w.emplace_back(json::value_t::binary); + CHECK(w.size() == 9); + } +}