Skip to content

Make scalar comparisons noexcept only when converting the scalar cannot throw - #5751

Merged
nlohmann merged 2 commits into
developfrom
fix/scalar-comparison-noexcept
Oct 2, 2026
Merged

nlohmann merged 2 commits into
developfrom
fix/scalar-comparison-noexcept

Conversation

@nlohmann

@nlohmann nlohmann commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #5750.

Problem

The comparison operators that take a scalar (==, !=, <, <=, >, >=, and C++20's <=>) convert the scalar with basic_json(scalar) and then compare two JSON values. They were all unconditionally noexcept, so a conversion that throws called std::terminate instead of propagating the exception:

enum class card { kreuz, andere };
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(card, {{card::kreuz, "kreuz"}})

json j = "kreuz";
bool b = (card::andere == j);  // to_json throws out_of_range.410 -> std::terminate()

The same happens with j == "text" if allocating the string throws std::bad_alloc. clang-tidy 22.1 reports the strict-enum case as bugprone-exception-escape; #5750 worked around it by rewriting the tests.

Change

  • The fix: the 16 scalar overloads are now noexcept(std::is_nothrow_constructible<basic_json, ScalarType>::value), so each one is noexcept exactly when the conversion is:
    • numbers, Booleans, nullptr, and plain enums: still noexcept
    • strings and enums whose to_json may throw (NLOHMANN_JSON_SERIALIZE_ENUM / _STRICT): now noexcept(false)
  • Unchanged: comparing two basic_json values.
  • Tests:
    • restore the strict-enum comparisons that Fix CI on develop #5750 removed
    • check that comparing an unmapped strict enum throws out_of_range.410 (it used to terminate)
    • pin the new exception specifications with static_asserts in unit-noexcept.cpp
  • Docs: all seven operator pages now state overload (2)'s exception safety and list the change in their version history (3.13.0).

Verification

  • Local test runs, all passing:
    • unit-conversions and unit-noexcept, as C++11 and C++20, with clang 21 and GCC 16
    • unit-comparison, as C++11, C++20, and C++20 with JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
  • clang-tidy 22.1.8 (CI's setup) is clean on both test files in the standards CI builds them in.
  • make check-amalgamation: passes.

Public API

This changes the documented exception specification of the scalar comparison operators, but no program that compiles today stops compiling:

  • Comparisons with numbers, Booleans, nullptr, and plain enums stay noexcept.
  • Comparisons with strings and with enums serialized by the enum macros become noexcept(false). In those cases the operator could already fail; it now throws instead of calling std::terminate.
  • Code that checks noexcept(j == "text") at compile time will see false.

The ABI is unaffected: a function's exception specification is not part of its mangled name.

🤖 Generated with Claude Code

@nlohmann
nlohmann added this pull request to stack #5752 October 2, 2026 08:19
@nlohmann nlohmann changed the title Make comparisons with scalars noexcept only when the conversion is Make scalar comparisons noexcept only when converting the scalar cannot throw Oct 2, 2026
Base automatically changed from fix/develop-ci-2026-10 to develop October 2, 2026 09:23
The comparison operators taking a scalar (==, !=, <, <=, >, >=, and
C++20's <=>) convert the scalar to a basic_json and compare, but were
unconditionally noexcept. When that conversion throws, the program
called std::terminate instead of propagating the exception, e.g. when
comparing a json with a string literal under memory pressure
(std::bad_alloc) or with an enum value not mapped by
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT (out_of_range.410). clang-tidy
22.1 reports the latter as bugprone-exception-escape.

Declare the 16 scalar overloads
noexcept(std::is_nothrow_constructible<basic_json, ScalarType>::value):
they stay noexcept for numbers, Booleans, nullptr, and plain enums,
and are noexcept(false) for strings and enums whose to_json may throw.
The comparisons of two basic_json values are unchanged.

Restore the strict-enum comparisons removed from unit-conversions.cpp
in the previous PR, check that comparing an unmapped strict enum now
throws, and pin the new exception specifications in unit-noexcept.cpp.
Document the exception safety of overload (2) on all seven operator
pages. Ran make amalgamate.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the fix/scalar-comparison-noexcept branch from 74216d4 to c0694f7 Compare October 2, 2026 09:24
Conflict in operator_ne.md: took develop's corrected page (#5638 documents that there is no C++20 member operator!=) and re-added this branch's conditional noexcept for overload (2) and its version history note. Ran make amalgamate.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 2, 2026
@nlohmann
nlohmann merged commit 6d0867a into develop Oct 2, 2026
64 of 164 checks passed
@nlohmann
nlohmann deleted the fix/scalar-comparison-noexcept branch October 2, 2026 15:25
nlohmann added a commit that referenced this pull request Oct 4, 2026
- binary_reader: rename the error_handler constructor parameter, which
  shadowed the member (-Wshadow, -Wshadow-field-in-constructor; #5746)
- basic_json(copy_construct_tag, ...): declare it noexcept when copying
  the base class is (GCC 16 -Wnoexcept; #5690)
- the scalar-on-left legacy comparison operators: noexcept only when
  converting the scalar is, like their member counterparts (#5682, #5751)
- compare_leaves: use std::is_eq/is_lt/is_gt instead of comparing a
  std::partial_ordering with 0 (-Wzero-as-null-pointer-constant; #5686)
- serializer: silence MSVC C4127 for the EnsureAscii template parameter
  (#5741, #5746)
- clang-tidy: return the sanitized reference in binary_writer, take the
  key of ordered_map::find_impl by const reference (#5727), and mark the
  switches over parse_array_index (#5728)
- ordered_map: keep <memory> for std::allocator (IWYU)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 4, 2026
* Keep the serializer conversion for objects whose keys cannot be converted

#5591 added a test converting nlohmann::json into a basic_json whose
string type cannot be constructed from std::string. That instantiates
convert_iteratively(), whose members.emplace_back(next.key(), ...) needs
exactly that key conversion, and broke the build of unit-alt-string.
Dispatch on the key's constructibility and leave such conversions to the
serializers, as the levels above the nesting bound already do (#3425).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix the remaining CI failures on develop

- unit-wstring: with a 16-bit wchar_t (Windows), a lone surrogate is
  reported as the ill-formed byte 0xFF since #5704; the std::wstring
  expectations still had the previous <U+0000>.
- ci_single_binaries: json_literals.hpp (#5610) and json.hpp include each
  other on purpose, and IWYU, not following the cycle, asks to replace
  json.hpp with json_fwd.hpp. Report its findings without failing the
  build, as already done for json.hpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix the library warnings and noexcept specifications from the merged PRs

- binary_reader: rename the error_handler constructor parameter, which
  shadowed the member (-Wshadow, -Wshadow-field-in-constructor; #5746)
- basic_json(copy_construct_tag, ...): declare it noexcept when copying
  the base class is (GCC 16 -Wnoexcept; #5690)
- the scalar-on-left legacy comparison operators: noexcept only when
  converting the scalar is, like their member counterparts (#5682, #5751)
- compare_leaves: use std::is_eq/is_lt/is_gt instead of comparing a
  std::partial_ordering with 0 (-Wzero-as-null-pointer-constant; #5686)
- serializer: silence MSVC C4127 for the EnsureAscii template parameter
  (#5741, #5746)
- clang-tidy: return the sanitized reference in binary_writer, take the
  key of ordered_map::find_impl by const reference (#5727), and mark the
  switches over parse_array_index (#5728)
- ordered_map: keep <memory> for std::allocator (IWYU)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Split unit-conversions.cpp so MinGW can link it

clang 18 with the MinGW linker failed to link test-conversions_cpp17
("relocation truncated to fit: IMAGE_REL_AMD64_REL32"). As windows.yml
recommends, keep the objects small by splitting the test file.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Fix the tests added by the merged PRs for all CI configurations

- discard the results of dump() and from_*() in CHECK_THROWS with
  utils::ignore_return_value (GCC -Werror=unused-result)
- give unit-bson's huge_string_t a default constructor (MSVC C2512,
  GCC 5, clang 3.5)
- unit-disabled_exceptions: use the literals namespace when the global
  UDLs are off (ci_test_noglobaludls; #5700)
- unit-binary_utf8_strict: expect the JSON pointer prefix with
  JSON_DIAGNOSTICS (#5741)
- skip the tests that rely on exceptions under JSON_NOEXCEPTION
  (#5678, #5732)
- clang-tidy and clang -Werror: static test data, CAPTURE(...);,
  const-correctness, use-after-move alias, unused conversion operator,
  a missing <iterator> include

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Title the macro examples and add JSON_STRICT_BINARY_UTF8 to the docset

The documentation style check requires "Example: ..." titles on pages with several examples (#5741, #5591) and a docset entry for every macro page.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Regenerate BUILD.bazel and nlohmann_json.natvis

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

#5746 added detail/output/error_handler.hpp and #5741 the json_abi_sbu8 ABI tag.

* Install libidn11 for the CMake 3.5.0 binary in ci_cmake_flags

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

#5733 moved ci_cmake_options from ubuntu:focal to ubuntu:24.04, which no longer ships libidn.so.11; the CMake 3.5.0 release binary links against it, so every ci_cmake_flags run has failed since. Install focal's libidn11 package for that matrix entry only.

* Suppress Infer's false STACK_VARIABLE_ADDRESS_ESCAPE in get_impl

get_impl() returns its local by value. A test added by the merged PRs instantiates it with a type Infer misreads, so ci_infer reported the 2021 code for the first time.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants