Compare commits

..
Author SHA1 Message Date
Niels Lohmann 864ac37cdb Add scalar-on-left overloads for legacy discarded comparisons in C++20
With JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1 and C++20, a scalar on
the left-hand side of <= or >= (e.g., `1 <= discarded`) yielded false
instead of the documented true. The C++20 legacy block only had member
operators, which are only candidates when the basic_json is the left
operand; for a scalar on the left, overload resolution picked the
candidate rewritten from operator<=>, which does not emulate the legacy
behavior. The C++17 branch already has scalar-on-the-left friend
overloads for <= and >=; add the equivalent pair to the C++20 legacy
block.

Added a regression test to tests/src/unit-comparison.cpp covering all
four operand orders for both operators.

Fixes #5665.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:16:44 +02:00
7 changed files with 70 additions and 69 deletions
@@ -50,11 +50,9 @@ The default value is `0` (disabled — existing behavior is preserved).
```
Code that relies on these producing arrays must use `json::array()` instead (see below). Lists with more than one
element, and a single `[string, value]` pair *written as a braced list*, such as `{{"key", "value"}}`, which still
creates an object, are not affected. This exception is based on how the pair is written, not on the shape of its
value: an existing JSON value that happens to be a two-element array with a string as its first element, such as
`json arr = {"key", 42};`, is still copied by `json j{arr};` rather than turned into an object. The library's own
conversions are not affected either: for example, `std::tuple<int>{5}` still becomes `[5]`.
element, and a single `[string, value]` pair such as `{{"key", "value"}}`, which still creates an object, are not
affected. The library's own conversions are not affected either: for example, `std::tuple<int>{5}` still becomes
`[5]`.
!!! note "ABI compatibility"
@@ -79,3 +79,6 @@ When the macro is not defined, the library will define it to its default value.
## Version history
- Added in version 3.11.0.
- Fixed in version 3.13.0 so `<=` and `>=` also emulate the legacy behavior in C++20 when the JSON value is the
right-hand operand of a scalar comparison; before, only the 3-way-comparison-rewritten candidate was found, which
yielded `#!cpp false` instead of `#!cpp true`.
-9
View File
@@ -34,7 +34,6 @@ class json_ref
json_ref(std::initializer_list<json_ref> init)
: owned_value(init)
, braced_list(true)
{}
template <
@@ -70,17 +69,9 @@ class json_ref
return &** this;
}
/// whether the value was written as a braced list, such as {"key", 1},
/// rather than given as a value
bool is_braced_list() const noexcept
{
return braced_list;
}
private:
mutable value_type owned_value = nullptr;
value_type const* value_ref = nullptr;
bool braced_list = false;
};
} // namespace detail
+21 -12
View File
@@ -1672,18 +1672,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
bool type_deduction = true,
value_t manual_type = value_t::array)
{
#if JSON_BRACE_INIT_COPY_SEMANTICS
// a single element that is a value rather than a braced list is
// copied or moved as is, whatever its content looks like
if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list())
{
*this = init.begin()->moved_or_copied();
set_parents();
assert_invariant();
return;
}
#endif
// check if each element is an array with two elements whose first
// element is a string
bool is_an_object = std::all_of(init.begin(), init.end(),
@@ -4685,6 +4673,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{
return *this >= basic_json(rhs);
}
// a scalar on the left-hand side would otherwise select the candidate
// rewritten from operator<=>, which does not emulate the legacy behavior
/// @brief comparison: less than or equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_le/
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) <= rhs;
}
/// @brief comparison: greater than or equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ge/
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) >= rhs;
}
#endif
#else
/// @brief comparison: equal
+21 -21
View File
@@ -20047,7 +20047,6 @@ class json_ref
json_ref(std::initializer_list<json_ref> init)
: owned_value(init)
, braced_list(true)
{}
template <
@@ -20083,17 +20082,9 @@ class json_ref
return &** this;
}
/// whether the value was written as a braced list, such as {"key", 1},
/// rather than given as a value
bool is_braced_list() const noexcept
{
return braced_list;
}
private:
mutable value_type owned_value = nullptr;
value_type const* value_ref = nullptr;
bool braced_list = false;
};
} // namespace detail
@@ -27762,18 +27753,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
bool type_deduction = true,
value_t manual_type = value_t::array)
{
#if JSON_BRACE_INIT_COPY_SEMANTICS
// a single element that is a value rather than a braced list is
// copied or moved as is, whatever its content looks like
if (type_deduction && init.size() == 1 && !init.begin()->is_braced_list())
{
*this = init.begin()->moved_or_copied();
set_parents();
assert_invariant();
return;
}
#endif
// check if each element is an array with two elements whose first
// element is a string
bool is_an_object = std::all_of(init.begin(), init.end(),
@@ -30775,6 +30754,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{
return *this >= basic_json(rhs);
}
// a scalar on the left-hand side would otherwise select the candidate
// rewritten from operator<=>, which does not emulate the legacy behavior
/// @brief comparison: less than or equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_le/
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) <= rhs;
}
/// @brief comparison: greater than or equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ge/
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) >= rhs;
}
#endif
#else
/// @brief comparison: equal
@@ -72,28 +72,6 @@ TEST_CASE("JSON_BRACE_INIT_COPY_SEMANTICS")
CHECK(j7 == json::array({1, 2}));
}
SECTION("single-element brace initialization copies a pair-shaped array value (#5662)")
{
// a JSON value that happens to be a 2-element array whose first
// element is a string must still be copied, not turned into an
// object; only a braced list written in the source, such as the
// inner {"key", "value"} of {{"key", "value"}}, describes an object
json const pair_shaped = json::array({"key", 42});
json const j1{pair_shaped};
CHECK(j1.is_array());
CHECK(j1 == pair_shaped);
json const j2 = {pair_shaped};
CHECK(j2.is_array());
CHECK(j2 == pair_shaped);
// the same holds for an rvalue of the same shape
json const j3{json::array({"key", 42})};
CHECK(j3.is_array());
CHECK(j3 == pair_shaped);
}
SECTION("what the macro does not change")
{
// lists with more than one element are unaffected
+22
View File
@@ -749,6 +749,28 @@ TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P24
CHECK_FALSE(j != i);
}
}
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
TEST_CASE("regression #5665 - scalar <= discarded and scalar >= discarded in C++20 legacy mode")
{
// Issue #5665: with a scalar on the left-hand side, <= and >= only had the
// candidate rewritten from operator<=>, which does not emulate the legacy
// discarded-value behavior. Check that scalar-on-the-left now matches the
// other three operand orders.
const json discarded(json::value_t::discarded);
const json one = 1;
CHECK(discarded <= 1);
CHECK(discarded >= 1);
CHECK(one <= discarded);
CHECK(one >= discarded);
CHECK(1 <= discarded);
CHECK(1 >= discarded);
CHECK(1.5 <= discarded);
CHECK(1.5 >= discarded);
}
#endif
#endif
namespace