mirror of
https://github.com/nlohmann/json.git
synced 2026-08-21 16:43:17 +00:00
Compare values without recursing, and without comparing them twice
Comparing two values compared their containers, which compare their elements, which brought the comparison back once per nesting level. Two values nested deeply enough exhausted the call stack and terminated the process with a segmentation fault - the same bug as #5387, in the last operation that still had it. Worse, an ordered comparison took exponentially long in the nesting depth before C++20. std::vector's operator< is a lexicographical comparison, which asks whether an element is less than its counterpart and then whether the counterpart is less than it - two full comparisons of everything below that element, at every level. Comparing two equal values nested 30 levels deep, which is nothing unusual, took 3.8 seconds; 40 levels would have taken an hour, and nothing about the value has to be pathological to get there. C++20 is unaffected: std::lexicographical_compare_three_way asks once. Compare a value that is nested too deeply to descend into on an explicit stack instead, in a single pass that yields less, equal, greater or unordered at once. Equality and the three-way comparison descend as they always did for the first 128 levels, which nothing measurable costs them; an ordered comparison no longer descends at all, which is what takes the exponent out of it. Objects and arrays that are not nested deeply are otherwise compared exactly as before. The results are unchanged for every pair of values: 68121 comparisons of a corpus that covers NaN, discarded values, mixed number types, binary values, empty containers and both object types are identical to develop, in C++11, C++17 and C++20, with and without thread_local storage and legacy discarded comparison. Reproducing that meant reproducing two subtleties: a lexicographic comparison steps over a pair it cannot order, where a three-way comparison stops at it, and an object compares its keys with < where its entries are ordered but with == where they are only checked for equality - not with the object's own comparator, which for nlohmann::ordered_map tells equality. Equality needs no ordering, so it no longer asks for any: a key or string type that can only be compared for equality still works. Measured (medians of 7 interleaved runs, clang -O3, C++11): comparing two equal values nested 30 levels deep 3778 ms -> 0.002 ms; ordering flat objects -33.6%; ordering flat arrays of numbers +27.3%, the one shape that pays for the single pass; equality unchanged throughout. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
+312
-7
@@ -846,6 +846,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
|
||||
return depth;
|
||||
}
|
||||
|
||||
/// @brief how many levels the comparison going on in this thread has descended into
|
||||
static std::size_t& compare_depth() noexcept
|
||||
{
|
||||
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
|
||||
return depth;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@@ -1194,6 +1201,286 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
}
|
||||
|
||||
|
||||
/// the result of comparing two values, including values that cannot be
|
||||
/// ordered at all, such as a discarded value or a NaN
|
||||
enum class compare_result { less, equal, greater, unordered };
|
||||
|
||||
/*!
|
||||
@brief counts one level of a comparison for as long as it runs
|
||||
|
||||
Does nothing without thread_local storage, where no descent is made at all.
|
||||
*/
|
||||
class compare_depth_guard
|
||||
{
|
||||
public:
|
||||
compare_depth_guard() noexcept
|
||||
{
|
||||
#ifndef JSON_NO_THREAD_LOCAL
|
||||
++compare_depth();
|
||||
#endif
|
||||
}
|
||||
|
||||
~compare_depth_guard() noexcept
|
||||
{
|
||||
#ifndef JSON_NO_THREAD_LOCAL
|
||||
--compare_depth();
|
||||
#endif
|
||||
}
|
||||
|
||||
compare_depth_guard(const compare_depth_guard&) = delete;
|
||||
compare_depth_guard& operator=(const compare_depth_guard&) = delete;
|
||||
compare_depth_guard(compare_depth_guard&&) = delete;
|
||||
compare_depth_guard& operator=(compare_depth_guard&&) = delete;
|
||||
};
|
||||
|
||||
/// @brief whether a comparison must stop descending and finish iteratively
|
||||
static bool compare_descent_exhausted() noexcept
|
||||
{
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// without a counter of its own per thread, the descent cannot be
|
||||
// bounded without racing another one, so none is made
|
||||
return true;
|
||||
#else
|
||||
return compare_depth() >= compare_depth_limit();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
/// @brief the ordering that @a result stands for
|
||||
static std::partial_ordering to_partial_ordering(compare_result result) noexcept // *NOPAD*
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case compare_result::less:
|
||||
return std::partial_ordering::less;
|
||||
case compare_result::greater:
|
||||
return std::partial_ordering::greater;
|
||||
case compare_result::equal:
|
||||
return std::partial_ordering::equivalent;
|
||||
case compare_result::unordered:
|
||||
default:
|
||||
return std::partial_ordering::unordered;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// the number of levels a comparison descends into before it compares what
|
||||
/// is left without the call stack
|
||||
static constexpr std::size_t compare_depth_limit()
|
||||
{
|
||||
return 128;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare two values that are not both an array or both an object
|
||||
|
||||
Such a pair is compared by the operators themselves, which cannot descend
|
||||
into it and therefore cannot recurse.
|
||||
*/
|
||||
template<bool Ordered>
|
||||
static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept
|
||||
{
|
||||
if (lhs == rhs)
|
||||
{
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
return order_leaves(lhs, rhs, std::integral_constant<bool, Ordered> {});
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare two object keys
|
||||
|
||||
An object compares its entries as pairs of a key and a value, so its keys
|
||||
are compared exactly as std::pair compares them: with < where the objects
|
||||
are being ordered, and with == where they are only checked for equality.
|
||||
Note that this is not the object's own comparator, which for a vector-backed
|
||||
object type such as nlohmann::ordered_map tells equality rather than order.
|
||||
*/
|
||||
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||
const typename object_t::key_type& rhs,
|
||||
std::true_type /*ordered*/)
|
||||
{
|
||||
if (lhs < rhs)
|
||||
{
|
||||
return compare_result::less;
|
||||
}
|
||||
|
||||
if (rhs < lhs)
|
||||
{
|
||||
return compare_result::greater;
|
||||
}
|
||||
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
/// @brief check two object keys for equality
|
||||
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||
const typename object_t::key_type& rhs,
|
||||
std::false_type /*ordered*/)
|
||||
{
|
||||
return lhs == rhs ? compare_result::equal : compare_result::unordered;
|
||||
}
|
||||
|
||||
/// @brief tell apart two values that are not equal
|
||||
/// @note only instantiated where the values are being ordered, as a key or
|
||||
/// string type is not required to be ordered to be compared for equality
|
||||
static compare_result order_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept
|
||||
{
|
||||
if (lhs < rhs)
|
||||
{
|
||||
return compare_result::less;
|
||||
}
|
||||
|
||||
if (rhs < lhs)
|
||||
{
|
||||
return compare_result::greater;
|
||||
}
|
||||
|
||||
return compare_result::unordered;
|
||||
}
|
||||
|
||||
/// @brief report two values as not equal without ordering them
|
||||
static compare_result order_leaves(const_reference /*lhs*/, const_reference /*rhs*/, std::false_type /*ordered*/) noexcept
|
||||
{
|
||||
return compare_result::unordered;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare @a lhs and @a rhs without descending into them
|
||||
|
||||
Reached once a comparison has descended @ref compare_depth_limit levels, so
|
||||
that comparing values cannot exhaust the call stack however deeply they are
|
||||
nested. The two values are walked in lockstep on an explicit stack and
|
||||
compared lexicographically, element by element in the order the containers
|
||||
enumerate them - which is how the container types this library ships compare
|
||||
themselves: a std::map enumerates its entries in key order, and
|
||||
nlohmann::ordered_map in insertion order. An object type that enumerates its
|
||||
entries in an unspecified order, such as std::unordered_map, compares them
|
||||
pairwise instead; the difference could only ever show below the bound.
|
||||
*/
|
||||
template<bool Ordered>
|
||||
static compare_result compare_iteratively(const_reference lhs, const_reference rhs,
|
||||
const bool unordered_compares_equal) noexcept
|
||||
{
|
||||
/// a pair of containers being compared in lockstep
|
||||
struct frame
|
||||
{
|
||||
const basic_json* lhs_value{nullptr};
|
||||
const basic_json* rhs_value{nullptr};
|
||||
typename array_t::const_iterator lhs_array_it{};
|
||||
typename array_t::const_iterator rhs_array_it{};
|
||||
typename object_t::const_iterator lhs_object_it{};
|
||||
typename object_t::const_iterator rhs_object_it{};
|
||||
};
|
||||
|
||||
std::vector<frame> stack;
|
||||
const basic_json* left = &lhs;
|
||||
const basic_json* right = &rhs;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const auto type = left->m_data.m_type;
|
||||
|
||||
if (type == right->m_data.m_type && (type == value_t::array || type == value_t::object))
|
||||
{
|
||||
// descend: the elements decide, and are compared further down
|
||||
stack.emplace_back();
|
||||
frame& pushed = stack.back();
|
||||
pushed.lhs_value = left;
|
||||
pushed.rhs_value = right;
|
||||
|
||||
if (type == value_t::array)
|
||||
{
|
||||
pushed.lhs_array_it = left->m_data.m_value.array->cbegin();
|
||||
pushed.rhs_array_it = right->m_data.m_value.array->cbegin();
|
||||
}
|
||||
else
|
||||
{
|
||||
pushed.lhs_object_it = left->m_data.m_value.object->cbegin();
|
||||
pushed.rhs_object_it = right->m_data.m_value.object->cbegin();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const compare_result result = compare_leaves<Ordered>(*left, *right);
|
||||
|
||||
// Values that cannot be ordered - a NaN, say - end an ordered
|
||||
// comparison for std::lexicographical_compare_three_way, but
|
||||
// std::lexicographical_compare treats them as equivalent and
|
||||
// carries on with the next element. Both are reproduced here,
|
||||
// so that a value nested too deeply to descend into compares
|
||||
// exactly as one that is not.
|
||||
if (result != compare_result::equal &&
|
||||
!(unordered_compares_equal && result == compare_result::unordered))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// walk back up past the containers that are exhausted, then take the
|
||||
// next pair of elements from the innermost one that is not
|
||||
for (;;)
|
||||
{
|
||||
if (stack.empty())
|
||||
{
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
frame& current = stack.back();
|
||||
const bool is_object = current.lhs_value->m_data.m_type == value_t::object;
|
||||
|
||||
const bool lhs_done = is_object
|
||||
? current.lhs_object_it == current.lhs_value->m_data.m_value.object->cend()
|
||||
: current.lhs_array_it == current.lhs_value->m_data.m_value.array->cend();
|
||||
const bool rhs_done = is_object
|
||||
? current.rhs_object_it == current.rhs_value->m_data.m_value.object->cend()
|
||||
: current.rhs_array_it == current.rhs_value->m_data.m_value.array->cend();
|
||||
|
||||
if (lhs_done || rhs_done)
|
||||
{
|
||||
// whichever ran out first holds the smaller container; if
|
||||
// both did, they are equal and the container above decides
|
||||
if (lhs_done != rhs_done)
|
||||
{
|
||||
return lhs_done ? compare_result::less : compare_result::greater;
|
||||
}
|
||||
|
||||
stack.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object)
|
||||
{
|
||||
// an entry is a key and a value, and the key decides first
|
||||
const compare_result key_result =
|
||||
compare_keys(current.lhs_object_it->first, current.rhs_object_it->first,
|
||||
std::integral_constant<bool, Ordered> {});
|
||||
|
||||
if (key_result != compare_result::equal)
|
||||
{
|
||||
return key_result;
|
||||
}
|
||||
|
||||
left = &(current.lhs_object_it->second);
|
||||
right = &(current.rhs_object_it->second);
|
||||
++current.lhs_object_it;
|
||||
++current.rhs_object_it;
|
||||
}
|
||||
else
|
||||
{
|
||||
left = &(*current.lhs_array_it);
|
||||
right = &(*current.rhs_array_it);
|
||||
++current.lhs_array_it;
|
||||
++current.rhs_array_it;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public:
|
||||
//////////////////////////
|
||||
// JSON parser callback //
|
||||
@@ -3986,7 +4273,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// because any negative signed value is smaller than any unsigned value.
|
||||
// Otherwise, the non-negative signed value is cast to unsigned before the
|
||||
// comparison to avoid wraparound.
|
||||
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \
|
||||
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \
|
||||
const auto lhs_type = lhs.type(); \
|
||||
const auto rhs_type = rhs.type(); \
|
||||
\
|
||||
@@ -3995,11 +4282,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
switch (lhs_type) \
|
||||
{ \
|
||||
case value_t::array: \
|
||||
{ \
|
||||
if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \
|
||||
{ \
|
||||
return (deep_result); \
|
||||
} \
|
||||
const compare_depth_guard guard; \
|
||||
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
||||
\
|
||||
} \
|
||||
\
|
||||
case value_t::object: \
|
||||
{ \
|
||||
if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \
|
||||
{ \
|
||||
return (deep_result); \
|
||||
} \
|
||||
const compare_depth_guard guard; \
|
||||
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
||||
\
|
||||
} \
|
||||
\
|
||||
case value_t::null: \
|
||||
return (null_result); \
|
||||
\
|
||||
@@ -4099,7 +4400,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
#endif
|
||||
const_reference lhs = *this;
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
@@ -4124,7 +4426,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
||||
std::partial_ordering::equivalent,
|
||||
std::partial_ordering::unordered,
|
||||
lhs_type <=> rhs_type) // *NOPAD*
|
||||
lhs_type <=> rhs_type, // *NOPAD*
|
||||
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
|
||||
}
|
||||
|
||||
/// @brief comparison: 3-way
|
||||
@@ -4191,7 +4494,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
#endif
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
@@ -4247,7 +4551,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// 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))
|
||||
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type),
|
||||
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
|
||||
}
|
||||
|
||||
/// @brief comparison: less than
|
||||
|
||||
@@ -22200,6 +22200,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
|
||||
return depth;
|
||||
}
|
||||
|
||||
/// @brief how many levels the comparison going on in this thread has descended into
|
||||
static std::size_t& compare_depth() noexcept
|
||||
{
|
||||
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
|
||||
return depth;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
@@ -22548,6 +22555,286 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
}
|
||||
|
||||
|
||||
/// the result of comparing two values, including values that cannot be
|
||||
/// ordered at all, such as a discarded value or a NaN
|
||||
enum class compare_result { less, equal, greater, unordered };
|
||||
|
||||
/*!
|
||||
@brief counts one level of a comparison for as long as it runs
|
||||
|
||||
Does nothing without thread_local storage, where no descent is made at all.
|
||||
*/
|
||||
class compare_depth_guard
|
||||
{
|
||||
public:
|
||||
compare_depth_guard() noexcept
|
||||
{
|
||||
#ifndef JSON_NO_THREAD_LOCAL
|
||||
++compare_depth();
|
||||
#endif
|
||||
}
|
||||
|
||||
~compare_depth_guard() noexcept
|
||||
{
|
||||
#ifndef JSON_NO_THREAD_LOCAL
|
||||
--compare_depth();
|
||||
#endif
|
||||
}
|
||||
|
||||
compare_depth_guard(const compare_depth_guard&) = delete;
|
||||
compare_depth_guard& operator=(const compare_depth_guard&) = delete;
|
||||
compare_depth_guard(compare_depth_guard&&) = delete;
|
||||
compare_depth_guard& operator=(compare_depth_guard&&) = delete;
|
||||
};
|
||||
|
||||
/// @brief whether a comparison must stop descending and finish iteratively
|
||||
static bool compare_descent_exhausted() noexcept
|
||||
{
|
||||
#ifdef JSON_NO_THREAD_LOCAL
|
||||
// without a counter of its own per thread, the descent cannot be
|
||||
// bounded without racing another one, so none is made
|
||||
return true;
|
||||
#else
|
||||
return compare_depth() >= compare_depth_limit();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if JSON_HAS_THREE_WAY_COMPARISON
|
||||
/// @brief the ordering that @a result stands for
|
||||
static std::partial_ordering to_partial_ordering(compare_result result) noexcept // *NOPAD*
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case compare_result::less:
|
||||
return std::partial_ordering::less;
|
||||
case compare_result::greater:
|
||||
return std::partial_ordering::greater;
|
||||
case compare_result::equal:
|
||||
return std::partial_ordering::equivalent;
|
||||
case compare_result::unordered:
|
||||
default:
|
||||
return std::partial_ordering::unordered;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// the number of levels a comparison descends into before it compares what
|
||||
/// is left without the call stack
|
||||
static constexpr std::size_t compare_depth_limit()
|
||||
{
|
||||
return 128;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare two values that are not both an array or both an object
|
||||
|
||||
Such a pair is compared by the operators themselves, which cannot descend
|
||||
into it and therefore cannot recurse.
|
||||
*/
|
||||
template<bool Ordered>
|
||||
static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept
|
||||
{
|
||||
if (lhs == rhs)
|
||||
{
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
return order_leaves(lhs, rhs, std::integral_constant<bool, Ordered> {});
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare two object keys
|
||||
|
||||
An object compares its entries as pairs of a key and a value, so its keys
|
||||
are compared exactly as std::pair compares them: with < where the objects
|
||||
are being ordered, and with == where they are only checked for equality.
|
||||
Note that this is not the object's own comparator, which for a vector-backed
|
||||
object type such as nlohmann::ordered_map tells equality rather than order.
|
||||
*/
|
||||
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||
const typename object_t::key_type& rhs,
|
||||
std::true_type /*ordered*/)
|
||||
{
|
||||
if (lhs < rhs)
|
||||
{
|
||||
return compare_result::less;
|
||||
}
|
||||
|
||||
if (rhs < lhs)
|
||||
{
|
||||
return compare_result::greater;
|
||||
}
|
||||
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
/// @brief check two object keys for equality
|
||||
static compare_result compare_keys(const typename object_t::key_type& lhs,
|
||||
const typename object_t::key_type& rhs,
|
||||
std::false_type /*ordered*/)
|
||||
{
|
||||
return lhs == rhs ? compare_result::equal : compare_result::unordered;
|
||||
}
|
||||
|
||||
/// @brief tell apart two values that are not equal
|
||||
/// @note only instantiated where the values are being ordered, as a key or
|
||||
/// string type is not required to be ordered to be compared for equality
|
||||
static compare_result order_leaves(const_reference lhs, const_reference rhs, std::true_type /*ordered*/) noexcept
|
||||
{
|
||||
if (lhs < rhs)
|
||||
{
|
||||
return compare_result::less;
|
||||
}
|
||||
|
||||
if (rhs < lhs)
|
||||
{
|
||||
return compare_result::greater;
|
||||
}
|
||||
|
||||
return compare_result::unordered;
|
||||
}
|
||||
|
||||
/// @brief report two values as not equal without ordering them
|
||||
static compare_result order_leaves(const_reference /*lhs*/, const_reference /*rhs*/, std::false_type /*ordered*/) noexcept
|
||||
{
|
||||
return compare_result::unordered;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief compare @a lhs and @a rhs without descending into them
|
||||
|
||||
Reached once a comparison has descended @ref compare_depth_limit levels, so
|
||||
that comparing values cannot exhaust the call stack however deeply they are
|
||||
nested. The two values are walked in lockstep on an explicit stack and
|
||||
compared lexicographically, element by element in the order the containers
|
||||
enumerate them - which is how the container types this library ships compare
|
||||
themselves: a std::map enumerates its entries in key order, and
|
||||
nlohmann::ordered_map in insertion order. An object type that enumerates its
|
||||
entries in an unspecified order, such as std::unordered_map, compares them
|
||||
pairwise instead; the difference could only ever show below the bound.
|
||||
*/
|
||||
template<bool Ordered>
|
||||
static compare_result compare_iteratively(const_reference lhs, const_reference rhs,
|
||||
const bool unordered_compares_equal) noexcept
|
||||
{
|
||||
/// a pair of containers being compared in lockstep
|
||||
struct frame
|
||||
{
|
||||
const basic_json* lhs_value{nullptr};
|
||||
const basic_json* rhs_value{nullptr};
|
||||
typename array_t::const_iterator lhs_array_it{};
|
||||
typename array_t::const_iterator rhs_array_it{};
|
||||
typename object_t::const_iterator lhs_object_it{};
|
||||
typename object_t::const_iterator rhs_object_it{};
|
||||
};
|
||||
|
||||
std::vector<frame> stack;
|
||||
const basic_json* left = &lhs;
|
||||
const basic_json* right = &rhs;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const auto type = left->m_data.m_type;
|
||||
|
||||
if (type == right->m_data.m_type && (type == value_t::array || type == value_t::object))
|
||||
{
|
||||
// descend: the elements decide, and are compared further down
|
||||
stack.emplace_back();
|
||||
frame& pushed = stack.back();
|
||||
pushed.lhs_value = left;
|
||||
pushed.rhs_value = right;
|
||||
|
||||
if (type == value_t::array)
|
||||
{
|
||||
pushed.lhs_array_it = left->m_data.m_value.array->cbegin();
|
||||
pushed.rhs_array_it = right->m_data.m_value.array->cbegin();
|
||||
}
|
||||
else
|
||||
{
|
||||
pushed.lhs_object_it = left->m_data.m_value.object->cbegin();
|
||||
pushed.rhs_object_it = right->m_data.m_value.object->cbegin();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const compare_result result = compare_leaves<Ordered>(*left, *right);
|
||||
|
||||
// Values that cannot be ordered - a NaN, say - end an ordered
|
||||
// comparison for std::lexicographical_compare_three_way, but
|
||||
// std::lexicographical_compare treats them as equivalent and
|
||||
// carries on with the next element. Both are reproduced here,
|
||||
// so that a value nested too deeply to descend into compares
|
||||
// exactly as one that is not.
|
||||
if (result != compare_result::equal &&
|
||||
!(unordered_compares_equal && result == compare_result::unordered))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// walk back up past the containers that are exhausted, then take the
|
||||
// next pair of elements from the innermost one that is not
|
||||
for (;;)
|
||||
{
|
||||
if (stack.empty())
|
||||
{
|
||||
return compare_result::equal;
|
||||
}
|
||||
|
||||
frame& current = stack.back();
|
||||
const bool is_object = current.lhs_value->m_data.m_type == value_t::object;
|
||||
|
||||
const bool lhs_done = is_object
|
||||
? current.lhs_object_it == current.lhs_value->m_data.m_value.object->cend()
|
||||
: current.lhs_array_it == current.lhs_value->m_data.m_value.array->cend();
|
||||
const bool rhs_done = is_object
|
||||
? current.rhs_object_it == current.rhs_value->m_data.m_value.object->cend()
|
||||
: current.rhs_array_it == current.rhs_value->m_data.m_value.array->cend();
|
||||
|
||||
if (lhs_done || rhs_done)
|
||||
{
|
||||
// whichever ran out first holds the smaller container; if
|
||||
// both did, they are equal and the container above decides
|
||||
if (lhs_done != rhs_done)
|
||||
{
|
||||
return lhs_done ? compare_result::less : compare_result::greater;
|
||||
}
|
||||
|
||||
stack.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object)
|
||||
{
|
||||
// an entry is a key and a value, and the key decides first
|
||||
const compare_result key_result =
|
||||
compare_keys(current.lhs_object_it->first, current.rhs_object_it->first,
|
||||
std::integral_constant<bool, Ordered> {});
|
||||
|
||||
if (key_result != compare_result::equal)
|
||||
{
|
||||
return key_result;
|
||||
}
|
||||
|
||||
left = &(current.lhs_object_it->second);
|
||||
right = &(current.rhs_object_it->second);
|
||||
++current.lhs_object_it;
|
||||
++current.rhs_object_it;
|
||||
}
|
||||
else
|
||||
{
|
||||
left = &(*current.lhs_array_it);
|
||||
right = &(*current.rhs_array_it);
|
||||
++current.lhs_array_it;
|
||||
++current.rhs_array_it;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public:
|
||||
//////////////////////////
|
||||
// JSON parser callback //
|
||||
@@ -25340,7 +25627,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// because any negative signed value is smaller than any unsigned value.
|
||||
// Otherwise, the non-negative signed value is cast to unsigned before the
|
||||
// comparison to avoid wraparound.
|
||||
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \
|
||||
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result, deep_result, may_descend) \
|
||||
const auto lhs_type = lhs.type(); \
|
||||
const auto rhs_type = rhs.type(); \
|
||||
\
|
||||
@@ -25349,11 +25636,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
switch (lhs_type) \
|
||||
{ \
|
||||
case value_t::array: \
|
||||
{ \
|
||||
if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \
|
||||
{ \
|
||||
return (deep_result); \
|
||||
} \
|
||||
const compare_depth_guard guard; \
|
||||
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
||||
\
|
||||
} \
|
||||
\
|
||||
case value_t::object: \
|
||||
{ \
|
||||
if (JSON_HEDLEY_UNLIKELY(!(may_descend) || compare_descent_exhausted())) \
|
||||
{ \
|
||||
return (deep_result); \
|
||||
} \
|
||||
const compare_depth_guard guard; \
|
||||
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
||||
\
|
||||
} \
|
||||
\
|
||||
case value_t::null: \
|
||||
return (null_result); \
|
||||
\
|
||||
@@ -25453,7 +25754,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
#endif
|
||||
const_reference lhs = *this;
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
@@ -25478,7 +25780,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
||||
std::partial_ordering::equivalent,
|
||||
std::partial_ordering::unordered,
|
||||
lhs_type <=> rhs_type) // *NOPAD*
|
||||
lhs_type <=> rhs_type, // *NOPAD*
|
||||
to_partial_ordering(compare_iteratively<true>(lhs, rhs, false)), true)
|
||||
}
|
||||
|
||||
/// @brief comparison: 3-way
|
||||
@@ -25545,7 +25848,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
#endif
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false)
|
||||
JSON_IMPLEMENT_OPERATOR( ==, true, false, false,
|
||||
compare_iteratively<false>(lhs, rhs, false) == compare_result::equal, true)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
@@ -25601,7 +25905,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
||||
// 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))
|
||||
JSON_IMPLEMENT_OPERATOR( <, false, false, operator<(lhs_type, rhs_type),
|
||||
compare_iteratively<true>(lhs, rhs, true) == compare_result::less, false)
|
||||
}
|
||||
|
||||
/// @brief comparison: less than
|
||||
|
||||
@@ -157,6 +157,54 @@ TEST_CASE("tests on deeply nested JSONs")
|
||||
CHECK(deep_depth == depth);
|
||||
}
|
||||
|
||||
SECTION("comparing")
|
||||
{
|
||||
// Comparing used to descend once per level, and an ordered
|
||||
// comparison used to compare every pair of elements twice, once in
|
||||
// each direction, which took exponentially long in the nesting
|
||||
// depth. Both are gone: these finish in milliseconds, where the
|
||||
// second used to take longer than anyone would wait even for a
|
||||
// value nested only a few dozen levels deep.
|
||||
const std::string text = std::string(depth, '[') + '0' + std::string(depth, ']');
|
||||
const json j = json::parse(text);
|
||||
const json same = json::parse(text);
|
||||
const json larger = json::parse(std::string(depth, '[') + '1' + std::string(depth, ']'));
|
||||
|
||||
CHECK(j == same);
|
||||
CHECK_FALSE(j == larger);
|
||||
CHECK(j != larger);
|
||||
|
||||
CHECK(j < larger);
|
||||
CHECK_FALSE(larger < j);
|
||||
CHECK(larger > j);
|
||||
CHECK(j <= same);
|
||||
CHECK(j >= same);
|
||||
|
||||
// a value that ends earlier is the smaller one
|
||||
const json shorter = json::parse(std::string(depth - 1, '[') + '0' + std::string(depth - 1, ']'));
|
||||
CHECK_FALSE(j == shorter);
|
||||
}
|
||||
|
||||
SECTION("comparing objects")
|
||||
{
|
||||
std::string text;
|
||||
text.reserve(6 * depth + 1);
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
text += "{\"a\":";
|
||||
}
|
||||
text += '1';
|
||||
text.append(depth, '}');
|
||||
|
||||
const json j = json::parse(text);
|
||||
const json same = json::parse(text);
|
||||
|
||||
CHECK(j == same);
|
||||
CHECK_FALSE(j != same);
|
||||
CHECK(j <= same);
|
||||
CHECK(j >= same);
|
||||
}
|
||||
|
||||
SECTION("the copy is independent of the original")
|
||||
{
|
||||
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
||||
|
||||
Reference in New Issue
Block a user