mirror of
https://github.com/nlohmann/json.git
synced 2026-09-27 18:20:32 +00:00
Fix stack overflow and exponential runtime when comparing nested values (#5390)
* 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> * Describe comparison in the no-thread-local docs and CI target Comparing two values now bounds its descent with a thread_local counter just as copying does, so the JSON_NO_THREAD_LOCAL page, the macro overview and the ci_test_no_thread_local target cover both rather than copying alone. Also record what switching the macro on costs a comparison: on the benchmark documents, comparing two equal values takes 10% to 90% longer. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Take the descent flag as an argument rather than testing it MSVC reports the test of a constant as C4127 ("conditional expression is constant"), which the Windows builds treat as an error: may_descend is false for operator<, so the operand short-circuits the whole condition. Passing it to compare_descent_exhausted() puts the test where the value is an ordinary parameter, and leaves the call sites with no condition of their own. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Note the comparison fallback in the no-thread-local documentation The macro page describes what the library defines JSON_NO_THREAD_LOCAL for by itself in terms of copying alone; comparing falls back the same way. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Parenthesise the reserve() computation in the comparison test clang-tidy reports the mixed * and + as readability-math-missing- parentheses, as it does for the identical line in the copy test. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use the shared descent bookkeeping rather than a second set Comparing kept a thread_local count, a limit and a guard of its own beside the ones copying already had, all three the same thing under a different name. They are gone; the shared count, limit and guard do the work. The guard grows a second constructor here, because the comparison operators are written as a macro and a macro cannot use the preprocessor: it cannot look the count up behind an #ifdef the way copy_structured does, so the guard looks it up for it. nesting_depth_exhausted() arrives for the same reason - whether an operator descends at all is a constant at every call site, and testing it there is what MSVC reports as C4127. Also say in compare_leaves what happens to a pair that is an array on one side and an object on the other, since the answer is not obvious from the code: an operator only descends into two values of the same type, so such a pair is told apart by its types alone - unequal, and ordered the way the types are - exactly as it is above the bound. And record what the explicit stack costs: the comparison operators are noexcept and the container comparison this replaces allocated nothing, so running out of memory here ends the process instead of throwing. It takes a value nested past the bound and an exhausted heap to reach, and the same comparison used to exhaust the call stack, but it is a new way to fail. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Amalgamate Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
+4
-4
@@ -300,10 +300,10 @@ add_custom_target(ci_test_skiplibraryversioncheck
|
|||||||
# Disable thread-local storage.
|
# Disable thread-local storage.
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Without thread-local storage, the copy constructor cannot bound its descent
|
# Without thread-local storage, copying and comparing cannot bound their
|
||||||
# and copies every object and array without the call stack. That path is
|
# descent and handle every object and array without the call stack. Those paths
|
||||||
# otherwise only reached by values nested deeper than the bound, so this target
|
# are otherwise only reached by values nested deeper than the bound, so this
|
||||||
# is what runs the whole test suite through it.
|
# target is what runs the whole test suite through them.
|
||||||
add_custom_target(ci_test_no_thread_local
|
add_custom_target(ci_test_no_thread_local
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
-DCMAKE_BUILD_TYPE=Debug -GNinja
|
-DCMAKE_BUILD_TYPE=Debug -GNinja
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
When defined, the library does not use `#!cpp thread_local` storage. This is relevant for the few environments whose
|
When defined, the library does not use `#!cpp thread_local` storage. This is relevant for the few environments whose
|
||||||
toolchain does not support it.
|
toolchain does not support it.
|
||||||
|
|
||||||
The copy constructor copies the first levels of a value by copying the containers, which copy their elements, and
|
Copying a value and comparing two values both descend into the first levels by letting the containers copy or compare
|
||||||
completes whatever is nested deeper than that without the call stack, so that copying a value cannot exhaust the stack
|
themselves, and finish whatever is nested deeper than that without the call stack, so that neither can exhaust the stack
|
||||||
however deeply it is nested. It counts the levels it has descended into in a `#!cpp thread_local` variable, as a counter
|
however deeply the values are nested. Each counts the levels it has descended into in a `#!cpp thread_local` variable, as
|
||||||
shared between threads would be raced.
|
a counter shared between threads would be raced.
|
||||||
|
|
||||||
Without that counter, no descent can be bounded safely, so objects and arrays are copied without the call stack right
|
Without those counters, no descent can be bounded safely, so objects and arrays are copied and compared without the call
|
||||||
away. Copying keeps working exactly as it does otherwise - the same values come out, and deeply nested values are copied
|
stack right away. Both keep working exactly as they do otherwise - the same values come out, the same comparisons hold,
|
||||||
just as safely - but copying is slower, because the containers no longer copy themselves. Copying the benchmark
|
and deeply nested values are handled just as safely - but both are slower, because the containers no longer copy or
|
||||||
documents takes 9% (`canada.json`) to 34% (`twitter.json`) longer; values built mostly from objects are affected the
|
compare themselves. Copying the benchmark documents takes 9% (`canada.json`) to 34% (`twitter.json`) longer, and
|
||||||
most.
|
comparing two equal ones 10% (`citm_catalog.json`) to 90% (`canada.json`) longer.
|
||||||
|
|
||||||
## Default definition
|
## Default definition
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ By default, `#!cpp JSON_NO_THREAD_LOCAL` is not defined.
|
|||||||
|
|
||||||
The library defines it by itself for Clang targeting MinGW, which does not survive the `#!cpp thread_local` storage:
|
The library defines it by itself for Clang targeting MinGW, which does not survive the `#!cpp thread_local` storage:
|
||||||
copying a value segfaults there, with both old and current Clang versions, while GCC targeting MinGW is unaffected.
|
copying a value segfaults there, with both old and current Clang versions, while GCC targeting MinGW is unaffected.
|
||||||
|
Copying and comparing fall back to working without the call stack there, as they do whenever the macro is defined.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md).
|
|||||||
|
|
||||||
## `JSON_NO_THREAD_LOCAL`
|
## `JSON_NO_THREAD_LOCAL`
|
||||||
|
|
||||||
When defined, the library does not use `#!cpp thread_local` storage. Copying a value then always avoids the call stack
|
When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then
|
||||||
rather than descending into a bounded number of levels first, which is slower but yields the same values.
|
always avoid the call stack rather than descending into a bounded number of levels first, which is slower but yields the
|
||||||
|
same values and the same comparisons.
|
||||||
|
|
||||||
See [full documentation of `JSON_NO_THREAD_LOCAL`](../api/macros/json_no_thread_local.md).
|
See [full documentation of `JSON_NO_THREAD_LOCAL`](../api/macros/json_no_thread_local.md).
|
||||||
|
|
||||||
|
|||||||
+297
-7
@@ -924,6 +924,31 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief whether a descent must stop here and finish without the call stack
|
||||||
|
|
||||||
|
@a may_descend says whether the operator descends at all; it is a constant
|
||||||
|
at every call site, and is passed rather than tested by the caller so that
|
||||||
|
the test does not become a constant condition there, which MSVC reports as
|
||||||
|
C4127.
|
||||||
|
|
||||||
|
The comparison operators use this rather than @ref nesting_depth_guard::okay,
|
||||||
|
because they are written as a macro and a macro cannot use the preprocessor
|
||||||
|
the way the guard's constructor does; @ref copy_structured, which can, asks
|
||||||
|
the guard instead and never calls this.
|
||||||
|
*/
|
||||||
|
static bool nesting_depth_exhausted(bool may_descend = true) noexcept
|
||||||
|
{
|
||||||
|
#ifdef JSON_NO_THREAD_LOCAL
|
||||||
|
// without a count of its own per thread, a descent cannot be bounded
|
||||||
|
// without racing another one, so none is made
|
||||||
|
static_cast<void>(may_descend);
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return !may_descend || nesting_depth() >= nesting_depth_limit();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief counts one level of a bounded descent for as long as it runs, and
|
@brief counts one level of a bounded descent for as long as it runs, and
|
||||||
reports whether the descent was still within the limit when it began
|
reports whether the descent was still within the limit when it began
|
||||||
@@ -1243,6 +1268,253 @@ 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 };
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@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.
|
||||||
|
|
||||||
|
That holds for a pair whose types differ as much as for a pair of leaves: an
|
||||||
|
array and an object are told apart by their types alone, because an operator
|
||||||
|
only ever descends into two values of the same type. So `==` reports them as
|
||||||
|
unequal without looking inside either, and an ordering falls back to the
|
||||||
|
order of the types - an object sorts before an array - exactly as it does
|
||||||
|
for a value that is not nested deeply enough to get here.
|
||||||
|
*/
|
||||||
|
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 nesting_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.
|
||||||
|
|
||||||
|
Note that the stack this walks with is allocated, while the comparison
|
||||||
|
operators are noexcept and the container comparison this replaces allocated
|
||||||
|
nothing. Failing that allocation therefore ends the process rather than
|
||||||
|
throwing. It only arises for values nested past the bound, and only when
|
||||||
|
memory has run out - where the same comparison used to exhaust the call
|
||||||
|
stack instead - but it is a way to fail that the operators did not have.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @brief restore the parent pointers after erasing from an object
|
/// @brief restore the parent pointers after erasing from an object
|
||||||
/// ordered_json keeps its members in a vector, and erasing a member
|
/// ordered_json keeps its members in a vector, and erasing a member
|
||||||
/// re-constructs every member after it in place, which resets their
|
/// re-constructs every member after it in place, which resets their
|
||||||
@@ -4183,7 +4455,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// because any negative signed value is smaller than any unsigned value.
|
// because any negative signed value is smaller than any unsigned value.
|
||||||
// Otherwise, the non-negative signed value is cast to unsigned before the
|
// Otherwise, the non-negative signed value is cast to unsigned before the
|
||||||
// comparison to avoid wraparound.
|
// 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 lhs_type = lhs.type(); \
|
||||||
const auto rhs_type = rhs.type(); \
|
const auto rhs_type = rhs.type(); \
|
||||||
\
|
\
|
||||||
@@ -4192,11 +4464,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
switch (lhs_type) \
|
switch (lhs_type) \
|
||||||
{ \
|
{ \
|
||||||
case value_t::array: \
|
case value_t::array: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
||||||
\
|
} \
|
||||||
|
\
|
||||||
case value_t::object: \
|
case value_t::object: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
||||||
\
|
} \
|
||||||
|
\
|
||||||
case value_t::null: \
|
case value_t::null: \
|
||||||
return (null_result); \
|
return (null_result); \
|
||||||
\
|
\
|
||||||
@@ -4296,7 +4582,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#endif
|
||||||
const_reference lhs = *this;
|
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__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -4321,7 +4608,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
||||||
std::partial_ordering::equivalent,
|
std::partial_ordering::equivalent,
|
||||||
std::partial_ordering::unordered,
|
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
|
/// @brief comparison: 3-way
|
||||||
@@ -4388,7 +4676,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#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__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -4444,7 +4733,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// default_result is used if we cannot compare values. In that case,
|
// default_result is used if we cannot compare values. In that case,
|
||||||
// we compare types. Note we have to call the operator explicitly,
|
// we compare types. Note we have to call the operator explicitly,
|
||||||
// because MSVC has problems otherwise.
|
// 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
|
/// @brief comparison: less than
|
||||||
|
|||||||
@@ -25569,6 +25569,31 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief whether a descent must stop here and finish without the call stack
|
||||||
|
|
||||||
|
@a may_descend says whether the operator descends at all; it is a constant
|
||||||
|
at every call site, and is passed rather than tested by the caller so that
|
||||||
|
the test does not become a constant condition there, which MSVC reports as
|
||||||
|
C4127.
|
||||||
|
|
||||||
|
The comparison operators use this rather than @ref nesting_depth_guard::okay,
|
||||||
|
because they are written as a macro and a macro cannot use the preprocessor
|
||||||
|
the way the guard's constructor does; @ref copy_structured, which can, asks
|
||||||
|
the guard instead and never calls this.
|
||||||
|
*/
|
||||||
|
static bool nesting_depth_exhausted(bool may_descend = true) noexcept
|
||||||
|
{
|
||||||
|
#ifdef JSON_NO_THREAD_LOCAL
|
||||||
|
// without a count of its own per thread, a descent cannot be bounded
|
||||||
|
// without racing another one, so none is made
|
||||||
|
static_cast<void>(may_descend);
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return !may_descend || nesting_depth() >= nesting_depth_limit();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief counts one level of a bounded descent for as long as it runs, and
|
@brief counts one level of a bounded descent for as long as it runs, and
|
||||||
reports whether the descent was still within the limit when it began
|
reports whether the descent was still within the limit when it began
|
||||||
@@ -25888,6 +25913,253 @@ 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 };
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@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.
|
||||||
|
|
||||||
|
That holds for a pair whose types differ as much as for a pair of leaves: an
|
||||||
|
array and an object are told apart by their types alone, because an operator
|
||||||
|
only ever descends into two values of the same type. So `==` reports them as
|
||||||
|
unequal without looking inside either, and an ordering falls back to the
|
||||||
|
order of the types - an object sorts before an array - exactly as it does
|
||||||
|
for a value that is not nested deeply enough to get here.
|
||||||
|
*/
|
||||||
|
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 nesting_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.
|
||||||
|
|
||||||
|
Note that the stack this walks with is allocated, while the comparison
|
||||||
|
operators are noexcept and the container comparison this replaces allocated
|
||||||
|
nothing. Failing that allocation therefore ends the process rather than
|
||||||
|
throwing. It only arises for values nested past the bound, and only when
|
||||||
|
memory has run out - where the same comparison used to exhaust the call
|
||||||
|
stack instead - but it is a way to fail that the operators did not have.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @brief restore the parent pointers after erasing from an object
|
/// @brief restore the parent pointers after erasing from an object
|
||||||
/// ordered_json keeps its members in a vector, and erasing a member
|
/// ordered_json keeps its members in a vector, and erasing a member
|
||||||
/// re-constructs every member after it in place, which resets their
|
/// re-constructs every member after it in place, which resets their
|
||||||
@@ -28828,7 +29100,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// because any negative signed value is smaller than any unsigned value.
|
// because any negative signed value is smaller than any unsigned value.
|
||||||
// Otherwise, the non-negative signed value is cast to unsigned before the
|
// Otherwise, the non-negative signed value is cast to unsigned before the
|
||||||
// comparison to avoid wraparound.
|
// 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 lhs_type = lhs.type(); \
|
||||||
const auto rhs_type = rhs.type(); \
|
const auto rhs_type = rhs.type(); \
|
||||||
\
|
\
|
||||||
@@ -28837,11 +29109,25 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
switch (lhs_type) \
|
switch (lhs_type) \
|
||||||
{ \
|
{ \
|
||||||
case value_t::array: \
|
case value_t::array: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
|
||||||
\
|
} \
|
||||||
|
\
|
||||||
case value_t::object: \
|
case value_t::object: \
|
||||||
|
{ \
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
|
||||||
|
{ \
|
||||||
|
return (deep_result); \
|
||||||
|
} \
|
||||||
|
const nesting_depth_guard guard; \
|
||||||
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
|
||||||
\
|
} \
|
||||||
|
\
|
||||||
case value_t::null: \
|
case value_t::null: \
|
||||||
return (null_result); \
|
return (null_result); \
|
||||||
\
|
\
|
||||||
@@ -28941,7 +29227,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#endif
|
||||||
const_reference lhs = *this;
|
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__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -28966,7 +29253,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
JSON_IMPLEMENT_OPERATOR(<=>, // *NOPAD*
|
||||||
std::partial_ordering::equivalent,
|
std::partial_ordering::equivalent,
|
||||||
std::partial_ordering::unordered,
|
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
|
/// @brief comparison: 3-way
|
||||||
@@ -29033,7 +29321,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
JSON_HEDLEY_DIAGNOSTIC_PUSH
|
||||||
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
JSON_HEDLEY_PRAGMA(GCC diagnostic ignored "-Wfloat-equal")
|
||||||
#endif
|
#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__
|
#ifdef __GNUC__
|
||||||
JSON_HEDLEY_DIAGNOSTIC_POP
|
JSON_HEDLEY_DIAGNOSTIC_POP
|
||||||
#endif
|
#endif
|
||||||
@@ -29089,7 +29378,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
|
|||||||
// default_result is used if we cannot compare values. In that case,
|
// default_result is used if we cannot compare values. In that case,
|
||||||
// we compare types. Note we have to call the operator explicitly,
|
// we compare types. Note we have to call the operator explicitly,
|
||||||
// because MSVC has problems otherwise.
|
// 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
|
/// @brief comparison: less than
|
||||||
|
|||||||
@@ -157,6 +157,54 @@ TEST_CASE("tests on deeply nested JSONs")
|
|||||||
CHECK(deep_depth == depth);
|
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")
|
SECTION("the copy is independent of the original")
|
||||||
{
|
{
|
||||||
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
const json j = json::parse(std::string(depth, '[') + '0' + std::string(depth, ']'));
|
||||||
|
|||||||
Reference in New Issue
Block a user