Compare commits

...

2 Commits

Author SHA1 Message Date
Niels Lohmann 8861d3ec5a 🎓 fix warning
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 11:45:42 +02:00
Niels Lohmann d8b274e7c2 Fix #3868: Remove operator!= to enable P2468R2 rewritten candidate synthesis
Under C++20 P2468R2, a hand-written operator!= suppresses the compiler's
rewritten-candidate synthesis for operator==, preventing heterogeneous
comparisons like `std::string s; json j; s == j;` from compiling.

Fix by removing the hand-written operator!=, allowing the compiler to
synthesize != as !(a==b) in all language modes (C++20 member functions
and pre-C++20 friend functions).

Behavior change: operator!= now returns !(a==b) unconditionally, including
for special values like NaN and discarded. This means:
- NaN != NaN now returns true (matches IEEE-754 semantics)
- discarded != x now returns true for any x (matches !(discarded == x))

This also fixes underlying defects in previously-working code:
- Restores direct == comparison for views vs json (reverts std::ranges::equal
  workaround added in PR #3950 to dodge this bug)
- Re-enables std::string == json comparisons (uncomments check in
  unit-constructor1.cpp)

Fixes: #3868, #3979

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 10:09:36 +02:00
6 changed files with 46 additions and 58 deletions
+11 -12
View File
@@ -19,10 +19,8 @@ class basic_json {
}; };
``` ```
1. Compares two JSON values for inequality according to the following rules: 1. Compares two JSON values for inequality. Returns `#!cpp !(lhs == rhs)` (until C++20) or `#!cpp !(*this == rhs)` (since C++20).
- The comparison always yields `#!cpp false` if (1) either operand is discarded, or (2) either operand is `NaN` and - This means the comparison is simply the logical negation of `operator==`, including for special values like `NaN` and `discarded`.
the other operand is either `NaN` or any other number.
- Otherwise, returns the result of `#!cpp !(lhs == rhs)` (until C++20) or `#!cpp !(*this == rhs)` (since C++20).
2. Compares a JSON value and a scalar or a scalar and a JSON value for inequality by converting the scalar to a JSON 2. Compares a JSON value and a scalar or a scalar and a JSON value for inequality by converting the scalar to a JSON
value and comparing both JSON values according to 1. value and comparing both JSON values according to 1.
@@ -54,13 +52,12 @@ Linear.
## Notes ## Notes
!!! note "Comparing `NaN`" !!! note "Comparing `NaN` and `discarded`"
`NaN` values are unordered within the domain of numbers. Since `operator!=` is defined as `!(a == b)`, the behavior for special values follows that of `operator==`:
The following comparisons all yield `#!cpp false`:
1. Comparing a `NaN` with itself. - For `NaN` values: `NaN == NaN` yields `#!cpp false`, so `NaN != NaN` yields `#!cpp true`.
2. Comparing a `NaN` with another `NaN`. - For `discarded` values: `discarded == x` yields `#!cpp false` for any `x`, so `discarded != x` yields `#!cpp true`.
3. Comparing a `NaN` and any other number.
## Examples ## Examples
@@ -94,5 +91,7 @@ Linear.
## Version history ## Version history
1. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. 1. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. Changed in version 3.12.x to remove
2. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. special-casing for `NaN` and `discarded` values; `operator!=` now consistently means `!(a == b)`.
2. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. Changed in version 3.12.x to remove
special-casing for `NaN` and `discarded` values; `operator!=` now consistently means `!(a == b)`.
-15
View File
@@ -3776,17 +3776,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *this == basic_json(rhs); return *this == basic_json(rhs);
} }
/// @brief comparison: not equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
bool operator!=(const_reference rhs) const noexcept
{
if (compares_unordered(rhs, true))
{
return false;
}
return !operator==(rhs);
}
/// @brief comparison: 3-way /// @brief comparison: 3-way
/// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/
std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD*
@@ -3892,10 +3881,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
friend bool operator!=(const_reference lhs, const_reference rhs) noexcept friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
{ {
if (compares_unordered(lhs, rhs, true))
{
return false;
}
return !(lhs == rhs); return !(lhs == rhs);
} }
-15
View File
@@ -24622,17 +24622,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *this == basic_json(rhs); return *this == basic_json(rhs);
} }
/// @brief comparison: not equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
bool operator!=(const_reference rhs) const noexcept
{
if (compares_unordered(rhs, true))
{
return false;
}
return !operator==(rhs);
}
/// @brief comparison: 3-way /// @brief comparison: 3-way
/// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/
std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD*
@@ -24738,10 +24727,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
friend bool operator!=(const_reference lhs, const_reference rhs) noexcept friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
{ {
if (compares_unordered(lhs, rhs, true))
{
return false;
}
return !(lhs == rhs); return !(lhs == rhs);
} }
+32 -13
View File
@@ -369,6 +369,7 @@ TEST_CASE("lexicographical comparison operators")
SECTION("comparison: not equal") SECTION("comparison: not equal")
{ {
// check that two values compare unequal as expected // check that two values compare unequal as expected
// operator!= now means exactly !(a==b) without special cases for NaN/discarded
for (size_t i = 0; i < j_values.size(); ++i) for (size_t i = 0; i < j_values.size(); ++i)
{ {
for (size_t j = 0; j < j_values.size(); ++j) for (size_t j = 0; j < j_values.size(); ++j)
@@ -376,25 +377,12 @@ TEST_CASE("lexicographical comparison operators")
CAPTURE(i) CAPTURE(i)
CAPTURE(j) CAPTURE(j)
if (json::compares_unordered(j_values[i], j_values[j], true))
{
// if two values compare unordered,
// check that the boolean comparison result is always false
CHECK_FALSE(j_values[i] != j_values[j]);
}
else
{
// otherwise, check that they compare according to their definition
// as the inverse of equal
CHECK((j_values[i] != j_values[j]) == !(j_values[i] == j_values[j])); CHECK((j_values[i] != j_values[j]) == !(j_values[i] == j_values[j]));
} }
} }
}
// compare with null pointer // compare with null pointer
const json j_null; const json j_null;
CHECK((j_null != nullptr) == false);
CHECK((nullptr != j_null) == false);
CHECK((j_null != nullptr) == !(j_null == nullptr)); CHECK((j_null != nullptr) == !(j_null == nullptr));
CHECK((nullptr != j_null) == !(nullptr == j_null)); CHECK((nullptr != j_null) == !(nullptr == j_null));
} }
@@ -594,3 +582,34 @@ TEST_CASE("lexicographical comparison operators")
} }
#endif #endif
} }
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)")
{
// Issue #3868: operator!= was preventing compiler from synthesizing reversed
// operator== candidates under C++20's P2468R2 rewritten candidate rules.
// Verify that heterogeneous comparisons now work.
SECTION("string vs json")
{
std::string s = "string";
json j = "string";
CHECK(s == j);
CHECK(j == s);
CHECK_FALSE(s != j);
CHECK_FALSE(j != s);
}
SECTION("other heterogeneous types")
{
int i = 42;
json j = 42;
CHECK(i == j);
CHECK(j == i);
CHECK_FALSE(i != j);
CHECK_FALSE(j != i);
}
}
#endif
+1 -1
View File
@@ -278,7 +278,7 @@ TEST_CASE("constructors")
const auto t = j.get<std::tuple<int, float, std::string>>(); const auto t = j.get<std::tuple<int, float, std::string>>();
CHECK(std::get<0>(t) == j[0]); CHECK(std::get<0>(t) == j[0]);
CHECK(std::get<1>(t) == j[1]); CHECK(std::get<1>(t) == j[1]);
// CHECK(std::get<2>(t) == j[2]); // commented out due to CI issue, see https://github.com/nlohmann/json/pull/3985 and https://github.com/nlohmann/json/issues/4025 CHECK(std::get<2>(t) == j[2]);
} }
SECTION("std::tuple tie") SECTION("std::tuple tie")
+1 -1
View File
@@ -942,7 +942,7 @@ TEST_CASE("iterators 2")
json j_expected{5, 4, 3, 2, 1}; json j_expected{5, 4, 3, 2, 1};
auto reversed = j | std::views::reverse; auto reversed = j | std::views::reverse;
CHECK(std::ranges::equal(reversed, j_expected)); CHECK(reversed == j_expected);
} }
SECTION("transform") SECTION("transform")