Skip to content

Legacy discarded comparison in C++20: scalar <= discarded and scalar >= discarded yield false #5665

Description

@nlohmann

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

  1. Save the program below as cf4.cpp.
  2. 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.
  3. 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

  • The bug also occurs if the latest version from the develop branch is used.
  • I can successfully compile and run the unit tests.

This issue was written by Claude Code on behalf of @nlohmann.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

kind: bugsolution: proposed fixa fix for the issue has been proposed and waits for confirmation

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions