Description
With JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON defined to 1 and C++20, the comparisons 1 <= discarded and 1 >= discarded return false when the scalar is on the left-hand side. The macro's documentation says they return true. The same expressions return true in C++17, and so do discarded <= 1, discarded >= 1 and json(1) <= discarded in C++20.
Where: in the C++20 branch, the legacy emulation consists of member functions only (include/nlohmann/json.hpp#L4631-L4676): operator<=(const_reference), operator<=(ScalarType), operator>=(const_reference) and operator>=(ScalarType).
Root cause: a member operator is only a candidate if the basic_json is the left operand, and C++20 only rewrites == and <=>, not <=/>=. For 1 <= discarded, the only viable candidate is therefore the one rewritten from discarded <=> 1 (#L4626). It yields partial_ordering::unordered and hence false. The C++17 branch has the scalar-on-the-left overloads operator<=(ScalarType, const_reference) and operator>=(ScalarType, const_reference); the C++20 legacy block has no counterpart. A side effect is that these expressions do not emit the deprecation warning that the legacy operators carry, so nothing hints at the different result.
Documentation: JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON lists <= and >= as true for "comparisons involving at least one discarded JSON value". Its C++20 note says that "Overloads for the equality and relational operators emulate the legacy behavior" and that only <=> is exempt.
Since when: since the C++20 legacy overloads were added in 3.11.0; reproduces with the v3.12.0 headers. It happens with Clang 21 and GCC 16.2.
Related: #1988 (background of the legacy mode). I found no issue or PR about the scalar-on-the-left case (searched for "JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON" and "legacy discarded comparison C++20").
Possible fix (tested): add the scalar-on-the-left overloads to the C++20 legacy block:
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) <= rhs;
}
template<typename ScalarType>
requires std::is_scalar_v<ScalarType>
friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) >= rhs;
}
A non-rewritten candidate wins over the rewritten one, so there is no ambiguity. What I tested with this change:
- The program below prints
true for all eight lines with Clang (C++17/20/23) and GCC 16.2 (C++20).
unit-comparison passes with the macro set to 0 and to 1, at C++11/17/20, with ASan/UBSan.
unit-comparison currently has no check with a scalar on the left of a discarded value; one could be added to its legacy section.
Reproduction steps
- Save the program below as
cf4.cpp.
- Compile it against
develop as C++20: clang++ -std=c++20 -I include cf4.cpp -o cf4. The expected deprecation warnings for the legacy operators appear for lines 16 and 17, but not for the scalar-on-the-left lines.
- Run
./cf4, and compare with -std=c++17.
Expected vs. actual results
- Expected: all eight lines print
true, as they do in C++17.
- Actual: in C++20,
1 <= discarded, 1 >= discarded, 1.5 <= discarded and 1.5 >= discarded print false.
Minimal code example
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 1
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
int main()
{
const json discarded(json::value_t::discarded);
const json one = 1;
// the documentation says <= and >= yield true if at least one operand is discarded
std::cout << std::boolalpha
<< "discarded <= 1: " << (discarded <= 1) << '\n' // true
<< "discarded >= 1: " << (discarded >= 1) << '\n' // true
<< "json(1) <= discarded: " << (one <= discarded) << '\n' // true
<< "json(1) >= discarded: " << (one >= discarded) << '\n' // true
<< "1 <= discarded: " << (1 <= discarded) << '\n' // expected true, C++20: false
<< "1 >= discarded: " << (1 >= discarded) << '\n' // expected true, C++20: false
<< "1.5 <= discarded: " << (1.5 <= discarded) << '\n' // expected true, C++20: false
<< "1.5 >= discarded: " << (1.5 >= discarded) << '\n'; // expected true, C++20: false
}
Error messages
Output with -std=c++20:
discarded <= 1: true
discarded >= 1: true
json(1) <= discarded: true
json(1) >= discarded: true
1 <= discarded: false
1 >= discarded: false
1.5 <= discarded: false
1.5 >= discarded: false
With -std=c++17, all eight lines print true.
Compiler and operating system
Apple clang 21.0.0 (clang-2100.3.34.2), macOS 27.0 (arm64); also GCC 16.2 (/opt/homebrew/bin/g++-16)
Library version
develop @ 633de8e; also 3.12.0
Validation
This issue was written by Claude Code on behalf of @nlohmann.
Description
With
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISONdefined to1and C++20, the comparisons1 <= discardedand1 >= discardedreturnfalsewhen the scalar is on the left-hand side. The macro's documentation says they returntrue. The same expressions returntruein C++17, and so dodiscarded <= 1,discarded >= 1andjson(1) <= discardedin C++20.Where: in the C++20 branch, the legacy emulation consists of member functions only (
include/nlohmann/json.hpp#L4631-L4676):operator<=(const_reference),operator<=(ScalarType),operator>=(const_reference)andoperator>=(ScalarType).Root cause: a member operator is only a candidate if the
basic_jsonis the left operand, and C++20 only rewrites==and<=>, not<=/>=. For1 <= discarded, the only viable candidate is therefore the one rewritten fromdiscarded <=> 1(#L4626). It yieldspartial_ordering::unorderedand hencefalse. The C++17 branch has the scalar-on-the-left overloadsoperator<=(ScalarType, const_reference)andoperator>=(ScalarType, const_reference); the C++20 legacy block has no counterpart. A side effect is that these expressions do not emit the deprecation warning that the legacy operators carry, so nothing hints at the different result.Documentation:
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISONlists<=and>=astruefor "comparisons involving at least one discarded JSON value". Its C++20 note says that "Overloads for the equality and relational operators emulate the legacy behavior" and that only<=>is exempt.Since when: since the C++20 legacy overloads were added in 3.11.0; reproduces with the v3.12.0 headers. It happens with Clang 21 and GCC 16.2.
Related: #1988 (background of the legacy mode). I found no issue or PR about the scalar-on-the-left case (searched for "JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON" and "legacy discarded comparison C++20").
Possible fix (tested): add the scalar-on-the-left overloads to the C++20 legacy block:
A non-rewritten candidate wins over the rewritten one, so there is no ambiguity. What I tested with this change:
truefor all eight lines with Clang (C++17/20/23) and GCC 16.2 (C++20).unit-comparisonpasses with the macro set to 0 and to 1, at C++11/17/20, with ASan/UBSan.unit-comparisoncurrently has no check with a scalar on the left of a discarded value; one could be added to its legacy section.Reproduction steps
cf4.cpp.developas C++20:clang++ -std=c++20 -I include cf4.cpp -o cf4. The expected deprecation warnings for the legacy operators appear for lines 16 and 17, but not for the scalar-on-the-left lines../cf4, and compare with-std=c++17.Expected vs. actual results
true, as they do in C++17.1 <= discarded,1 >= discarded,1.5 <= discardedand1.5 >= discardedprintfalse.Minimal code example
Error messages
Output with
-std=c++20:With
-std=c++17, all eight lines printtrue.Compiler and operating system
Apple clang 21.0.0 (clang-2100.3.34.2), macOS 27.0 (arm64); also GCC 16.2 (/opt/homebrew/bin/g++-16)
Library version
develop@ 633de8e; also 3.12.0Validation
developbranch is used.This issue was written by Claude Code on behalf of @nlohmann.