diff --git a/include/nlohmann/detail/value_t.hpp b/include/nlohmann/detail/value_t.hpp index 9fc217bcc..cda0facd2 100644 --- a/include/nlohmann/detail/value_t.hpp +++ b/include/nlohmann/detail/value_t.hpp @@ -117,6 +117,34 @@ inline bool operator<(const value_t lhs, const value_t rhs) noexcept } #endif +/*! +@brief whether a value of the given type owns heap memory + +Strings, binaries, objects, and arrays are stored as pointers to allocated +values; all other types are stored directly in the union. Every enumerator is +listed so that -Wswitch-enum flags this function when a new type is added. +*/ +inline bool owns_memory(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + return true; + + 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: + return false; + } +} + /*! @brief compare an integer with a floating point number without precision loss diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index f822b1406..3dc609038 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: } } + // keep the check whether t owns memory 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(detail::owns_memory(t))) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { @@ -2559,15 +2584,14 @@ 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(!detail::owns_memory(m_data.m_type))) { - // 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 +2823,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,7 +5213,22 @@ public: /// @name lexicographical comparison operators /// @{ - // note parentheses around operands are necessary; see + // 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 the container operands are necessary, because + // GCC otherwise parses `array <` as std::array; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value // is negative before casting. If it is, the comparison is performed with @@ -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,173 @@ public: return compares_unordered(*this, rhs, inverse); } + /// @brief whether objects and arrays are the standard containers, whose + /// elements equal_bounded() may therefore compare itself + using standard_object_t = std::map>>; + using standard_array_t = std::vector>; + using equal_bounded_containers = detail::conjunction, std::is_same>; + + // 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 + + /// @brief compare two leaves of the same type inside equal_structured() + /// @return 1 if they are equal, 0 if they are not, and -1 if the values are + /// of different types or are not handled here, which this leaves to + /// equal_structured() + /// @note Unlike JSON_IMPLEMENT_SCALAR_FAST, this also compares strings: + /// they are the most common leaves in a document, so comparing them + /// inline pays off in the container walk, while the operators keep + /// their inlined code at each call site small. Binary values are + /// rare and left to equal_general(). + JSON_HEDLEY_ALWAYS_INLINE + static int equal_leaf(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (lhs.m_data.m_type == rhs.m_data.m_type) + { + switch (lhs.m_data.m_type) + { + case value_t::null: + return 1; + case value_t::boolean: + return lhs.m_data.m_value.boolean == rhs.m_data.m_value.boolean; + case value_t::number_integer: + return lhs.m_data.m_value.number_integer == rhs.m_data.m_value.number_integer; + case value_t::number_unsigned: + return lhs.m_data.m_value.number_unsigned == rhs.m_data.m_value.number_unsigned; + case value_t::number_float: + return lhs.m_data.m_value.number_float == rhs.m_data.m_value.number_float; + case value_t::string: + return *lhs.m_data.m_value.string == *rhs.m_data.m_value.string; + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + return -1; + } + + /// @brief compare for equality, walking objects and arrays here instead of + /// in their containers so that the depth is a parameter and needs no + /// thread-local count; only used for the standard containers + static bool equal_bounded(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const int leaf = equal_leaf(lhs, rhs); + return leaf < 0 ? equal_structured(lhs, rhs, depth) : leaf != 0; + } + + static bool equal_structured(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const auto type = lhs.m_data.m_type; + if (type != rhs.m_data.m_type || (type != value_t::array && type != value_t::object)) + { + return equal_general(lhs, rhs); + } + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return compare_iteratively(lhs, rhs, false) == compare_result::equal; + } + + if (type == value_t::array) + { + const auto& a = *lhs.m_data.m_value.array; + const auto& b = *rhs.m_data.m_value.array; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + const int leaf = equal_leaf(*it_a, *it_b); + if (leaf < 0 ? !equal_structured(*it_a, *it_b, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + const auto& a = *lhs.m_data.m_value.object; + const auto& b = *rhs.m_data.m_value.object; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + if (!(it_a->first == it_b->first)) + { + return false; + } + const int leaf = equal_leaf(it_a->second, it_b->second); + if (leaf < 0 ? !equal_structured(it_a->second, it_b->second, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::true_type /*standard containers*/) noexcept + { + return equal_bounded(lhs, rhs, 0); + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::false_type /*other containers*/) noexcept + { + return equal_general(lhs, rhs); + } + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -5308,11 +5535,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_dispatch(lhs, rhs, equal_bounded_containers {}); } /// @brief comparison: equal @@ -5329,13 +5556,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 +5645,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_dispatch(lhs, rhs, equal_bounded_containers {}); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -5482,11 +5704,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 +5816,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 ce6158163..8c36c353f 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -3338,6 +3338,34 @@ inline bool operator<(const value_t lhs, const value_t rhs) noexcept } #endif +/*! +@brief whether a value of the given type owns heap memory + +Strings, binaries, objects, and arrays are stored as pointers to allocated +values; all other types are stored directly in the union. Every enumerator is +listed so that -Wswitch-enum flags this function when a new type is added. +*/ +inline bool owns_memory(const value_t t) noexcept +{ + switch (t) + { + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + return true; + + 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: + return false; + } +} + /*! @brief compare an integer with a floating point number without precision loss @@ -30366,26 +30394,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; } @@ -30432,6 +30446,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)) {} @@ -30692,7 +30734,18 @@ public: } } + // keep the check whether t owns memory 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(detail::owns_memory(t))) + { + destroy_owned(t); + } + } + + JSON_HEDLEY_NEVER_INLINE void destroy_owned(value_t t) { switch (t) { @@ -32340,15 +32393,14 @@ 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(!detail::owns_memory(m_data.m_type))) { - // 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(); @@ -32580,6 +32632,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 { @@ -34954,7 +35022,22 @@ public: /// @name lexicographical comparison operators /// @{ - // note parentheses around operands are necessary; see + // 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 the container operands are necessary, because + // GCC otherwise parses `array <` as std::array; see // https://github.com/nlohmann/json/issues/1530 // Mixed signed/unsigned integer comparisons check whether the signed value // is negative before casting. If it is, the comparison is performed with @@ -34990,24 +35073,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); \ \ @@ -35051,6 +35121,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 @@ -35078,6 +35166,173 @@ public: return compares_unordered(*this, rhs, inverse); } + /// @brief whether objects and arrays are the standard containers, whose + /// elements equal_bounded() may therefore compare itself + using standard_object_t = std::map>>; + using standard_array_t = std::vector>; + using equal_bounded_containers = detail::conjunction, std::is_same>; + + // 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 + + /// @brief compare two leaves of the same type inside equal_structured() + /// @return 1 if they are equal, 0 if they are not, and -1 if the values are + /// of different types or are not handled here, which this leaves to + /// equal_structured() + /// @note Unlike JSON_IMPLEMENT_SCALAR_FAST, this also compares strings: + /// they are the most common leaves in a document, so comparing them + /// inline pays off in the container walk, while the operators keep + /// their inlined code at each call site small. Binary values are + /// rare and left to equal_general(). + JSON_HEDLEY_ALWAYS_INLINE + static int equal_leaf(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_PUSH + JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal") +#endif + if (lhs.m_data.m_type == rhs.m_data.m_type) + { + switch (lhs.m_data.m_type) + { + case value_t::null: + return 1; + case value_t::boolean: + return lhs.m_data.m_value.boolean == rhs.m_data.m_value.boolean; + case value_t::number_integer: + return lhs.m_data.m_value.number_integer == rhs.m_data.m_value.number_integer; + case value_t::number_unsigned: + return lhs.m_data.m_value.number_unsigned == rhs.m_data.m_value.number_unsigned; + case value_t::number_float: + return lhs.m_data.m_value.number_float == rhs.m_data.m_value.number_float; + case value_t::string: + return *lhs.m_data.m_value.string == *rhs.m_data.m_value.string; + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + } +#ifdef __GNUC__ + JSON_HEDLEY_DIAGNOSTIC_POP +#endif + return -1; + } + + /// @brief compare for equality, walking objects and arrays here instead of + /// in their containers so that the depth is a parameter and needs no + /// thread-local count; only used for the standard containers + static bool equal_bounded(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const int leaf = equal_leaf(lhs, rhs); + return leaf < 0 ? equal_structured(lhs, rhs, depth) : leaf != 0; + } + + static bool equal_structured(const_reference lhs, const_reference rhs, std::size_t depth) noexcept + { + const auto type = lhs.m_data.m_type; + if (type != rhs.m_data.m_type || (type != value_t::array && type != value_t::object)) + { + return equal_general(lhs, rhs); + } + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return compare_iteratively(lhs, rhs, false) == compare_result::equal; + } + + if (type == value_t::array) + { + const auto& a = *lhs.m_data.m_value.array; + const auto& b = *rhs.m_data.m_value.array; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + const int leaf = equal_leaf(*it_a, *it_b); + if (leaf < 0 ? !equal_structured(*it_a, *it_b, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + const auto& a = *lhs.m_data.m_value.object; + const auto& b = *rhs.m_data.m_value.object; + if (a.size() != b.size()) + { + return false; + } + auto it_b = b.cbegin(); + for (auto it_a = a.cbegin(); it_a != a.cend(); ++it_a, ++it_b) + { + if (!(it_a->first == it_b->first)) + { + return false; + } + const int leaf = equal_leaf(it_a->second, it_b->second); + if (leaf < 0 ? !equal_structured(it_a->second, it_b->second, depth + 1) : leaf == 0) + { + return false; + } + } + return true; + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::true_type /*standard containers*/) noexcept + { + return equal_bounded(lhs, rhs, 0); + } + + static bool equal_dispatch(const_reference lhs, const_reference rhs, std::false_type /*other containers*/) noexcept + { + return equal_general(lhs, rhs); + } + public: #if JSON_HAS_THREE_WAY_COMPARISON /// @brief comparison: equal @@ -35089,11 +35344,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_dispatch(lhs, rhs, equal_bounded_containers {}); } /// @brief comparison: equal @@ -35110,13 +35365,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 @@ -35204,11 +35454,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_dispatch(lhs, rhs, equal_bounded_containers {}); } // The friend comparisons with a scalar name the JSON type via decltype of @@ -35263,11 +35513,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 @@ -35378,6 +35625,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); + } +}