From 45b837885e519bdda7f8c329b03e0a1e45fb5708 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 13 Oct 2026 18:57:07 +0200 Subject: [PATCH] Allow the same type for number_integer_t and number_unsigned_t basic_json did not compile with NumberUnsignedType == NumberIntegerType because overloads on both types collided. Such a basic_json now stores every integer as number_integer: the colliding overloads take a placeholder type, unsigned C++ integers are converted to number_integer_t, and value_t::number_unsigned maps to number_integer. Also fixes two problems this configuration would hit: the unsigned integer parser accepted values that wrap around to negative numbers, and dump() overflowed when computing the absolute value of the minimum. A repeated NumberFloatType gets a static_assert. Refs #2573 Signed-off-by: Niels Lohmann --- .../docs/api/basic_json/is_number_unsigned.md | 3 + .../docs/api/basic_json/number_unsigned_t.md | 4 +- .../docs/features/types/number_handling.md | 7 +- .../features/types/template_parameters.md | 55 +++-- .../nlohmann/detail/conversions/from_json.hpp | 2 +- .../nlohmann/detail/conversions/to_json.hpp | 4 +- .../nlohmann/detail/input/number_parse.hpp | 21 +- include/nlohmann/detail/meta/type_traits.hpp | 30 +++ .../nlohmann/detail/output/binary_writer.hpp | 6 +- include/nlohmann/detail/output/serializer.hpp | 27 +-- include/nlohmann/json.hpp | 32 ++- single_include/nlohmann/json.hpp | 122 ++++++++--- tests/src/unit-same-number-types.cpp | 202 ++++++++++++++++++ 13 files changed, 430 insertions(+), 85 deletions(-) create mode 100644 tests/src/unit-same-number-types.cpp diff --git a/docs/mkdocs/docs/api/basic_json/is_number_unsigned.md b/docs/mkdocs/docs/api/basic_json/is_number_unsigned.md index 50083164e..4dfb99049 100644 --- a/docs/mkdocs/docs/api/basic_json/is_number_unsigned.md +++ b/docs/mkdocs/docs/api/basic_json/is_number_unsigned.md @@ -6,6 +6,9 @@ constexpr bool is_number_unsigned() const noexcept; This function returns `#!cpp true` if and only if the JSON value is an unsigned integer number. This excludes floating-point and signed integer values. + +If [`number_unsigned_t`](number_unsigned_t.md) is the same type as [`number_integer_t`](number_integer_t.md), every +integer is stored as `number_integer_t`, and this function always returns `#!cpp false`. ## Return value diff --git a/docs/mkdocs/docs/api/basic_json/number_unsigned_t.md b/docs/mkdocs/docs/api/basic_json/number_unsigned_t.md index 81cc00f77..1f2c80467 100644 --- a/docs/mkdocs/docs/api/basic_json/number_unsigned_t.md +++ b/docs/mkdocs/docs/api/basic_json/number_unsigned_t.md @@ -25,7 +25,8 @@ the type to use. `NumberUnsignedType` : the type to store unsigned integers. It must be an **unsigned integral** type (`#!cpp std::is_integral`) with a `#!cpp std::numeric_limits` specialization, and it must be able to represent the absolute value of every - [`number_integer_t`](number_integer_t.md) value. See + [`number_integer_t`](number_integer_t.md) value. Alternatively, it can be the same type as + [`number_integer_t`](number_integer_t.md); then every integer is stored as `number_integer_t`. See [Template Parameter Requirements](../../features/types/template_parameters.md#numberintegertype-and-numberunsignedtype). ## Notes @@ -89,3 +90,4 @@ Integer number values are stored directly inside a `basic_json` type. ## Version history - Added in version 2.0.0. +- Can be the same type as `number_integer_t` since version 3.13.0. diff --git a/docs/mkdocs/docs/features/types/number_handling.md b/docs/mkdocs/docs/features/types/number_handling.md index 13b6dfd43..911ffbc78 100644 --- a/docs/mkdocs/docs/features/types/number_handling.md +++ b/docs/mkdocs/docs/features/types/number_handling.md @@ -343,8 +343,11 @@ The number types can be changed with template parameters. in case of overflow. - The type for unsigned integers must be convertible from `#!c unsigned long long`. The type for floating-point numbers is used in case of overflow. - - The types for signed and unsigned integers must be distinct, see - [#2573](https://github.com/nlohmann/json/issues/2573). + - The type for unsigned integers can also be the same signed type as the type for signed integers (e.g., + `#!c std::int64_t` for both). Then every integer is stored as a signed integer, and + [`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!c false`, see + [Template Parameter Requirements](template_parameters.md#same-type-for-both). + - The type for floating-point numbers must be distinct from the integer types. - Only `#!c double`, `#!c float`, and `#!c long double` are supported for floating-point numbers. !!! example diff --git a/docs/mkdocs/docs/features/types/template_parameters.md b/docs/mkdocs/docs/features/types/template_parameters.md index f20102878..c57bb314f 100644 --- a/docs/mkdocs/docs/features/types/template_parameters.md +++ b/docs/mkdocs/docs/features/types/template_parameters.md @@ -40,19 +40,19 @@ Requirements are split into two groups: ## Overview -| Template parameter | Default | Notable substitutes | -|-------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------| -| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps | -| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` | -| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` | -| [`BooleanType`](#booleantype) | `bool` | none worth using | -| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type | -| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType` | -| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) | -| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators | -| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface | -| [`BinaryType`](#binarytype) | `#!cpp std::vector` | `#!cpp std::vector` | -| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class | +| Template parameter | Default | Notable substitutes | +|-------------------------------------------------------------------|-----------------------------------|--------------------------------------------------------------------------------------------------| +| [`ObjectType`](#objecttype) | `std::map` | [`nlohmann::ordered_map`](../../api/ordered_map.md), Abseil hash maps | +| [`ArrayType`](#arraytype) | `std::vector` | `#!cpp std::deque` | +| [`StringType`](#stringtype) | `std::string` | `std::string`-like types over `char` | +| [`BooleanType`](#booleantype) | `bool` | none worth using | +| [`NumberIntegerType`](#numberintegertype-and-numberunsignedtype) | `std::int64_t` | any signed integer type | +| [`NumberUnsignedType`](#numberintegertype-and-numberunsignedtype) | `std::uint64_t` | any unsigned integer type at least as wide as `NumberIntegerType`, or `NumberIntegerType` itself | +| [`NumberFloatType`](#numberfloattype) | `double` | `float` (`long double`: no binary formats) | +| [`AllocatorType`](#allocatortype) | `std::allocator` | stateless allocators | +| [`JSONSerializer`](#jsonserializer) | `adl_serializer` | serializers with the same interface | +| [`BinaryType`](#binarytype) | `#!cpp std::vector` | `#!cpp std::vector` | +| [`CustomBaseClass`](#custombaseclass) | `void` | any default-constructible class | !!! warning "Third-party containers and incomplete types" @@ -499,13 +499,14 @@ Both types are stored **directly** inside `basic_json`'s union. ### Always required - `#!cpp std::is_integral` must be satisfied: `NumberIntegerType` must be a **signed** integer type, - `NumberUnsignedType` an **unsigned** integer type. Class types are not supported -- among others, the constructors - taking integer values are constrained on `#!cpp std::is_integral`. + `NumberUnsignedType` an **unsigned** integer type or the same type as `NumberIntegerType` (see + [below](#same-type-for-both)). Class types are not supported -- among others, the constructors taking integer values + are constrained on `#!cpp std::is_integral`. - Trivially default-constructible, trivially copyable, and trivially destructible (union member). - `#!cpp std::numeric_limits` must be specialized for both types. -- `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value; serialization - of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to be at least as - wide as `NumberIntegerType`, which is what that amounts to for the standard integer types. +- An unsigned `NumberUnsignedType` must be able to represent the absolute value of every `NumberIntegerType` value; + serialization of negative numbers converts the value to `NumberUnsignedType`. A `#!cpp static_assert` requires it to + be at least as wide as `NumberIntegerType`, which is what that amounts to for the standard integer types. - Both types must fit into the internal 64-character number buffer used by [`dump`](../../api/basic_json/dump.md), which is the case for all standard integer types. - [`std::hash`](../../api/basic_json/std_hash.md) additionally requires `#!cpp std::hash` specializations. @@ -517,6 +518,23 @@ is stored as [`number_float_t`](../../api/basic_json/number_float_t.md) instead. therefore silently changes parse results rather than raising an error. See [Number Handling](number_handling.md) for details. +#### Same type for both + +`NumberUnsignedType` may be the same type as `NumberIntegerType`, e.g. +`#!cpp basic_json`, for code that only +wants to handle one integer type. Such a `basic_json` stores every integer as +[`number_integer_t`](../../api/basic_json/number_integer_t.md): + +- [`is_number_unsigned()`](../../api/basic_json/is_number_unsigned.md) is always `#!cpp false`, and + [`type()`](../../api/basic_json/type.md) never returns `value_t::number_unsigned`. Constructing a value from + `value_t::number_unsigned` gives a `value_t::number_integer` value. +- Values of unsigned C++ types (e.g., `#!cpp 5u`) are converted to `NumberIntegerType`, so values above its maximum + wrap around, as values outside the range of a narrower `NumberIntegerType` already do. +- The parsers store a non-negative integer above the maximum of `NumberIntegerType` (e.g., `18446744073709551615` for + `#!cpp std::int64_t`) as [`number_float_t`](../../api/basic_json/number_float_t.md), like any other integer that does + not fit. +- Converting from a `basic_json` with distinct types turns its unsigned values into `number_integer_t` values. + ### Compatible types | Type pair | Support | @@ -525,6 +543,7 @@ therefore silently changes parse results rather than raising an error. See | `#!cpp std::int32_t` / `#!cpp std::uint32_t`, `#!cpp long long` / `#!cpp unsigned long long` | full; narrower types change which literals the parser can represent | | any other pair of standard signed/unsigned integer types | full | | class types, enumerations | not usable; `#!cpp std::is_integral` must hold | +| the same signed integer type for both, e.g. `#!cpp std::int64_t` / `#!cpp std::int64_t` | full; every integer is stored as `number_integer_t`, see [above](#same-type-for-both) | | `#!cpp bool`, or a type already used for another member of the union | not usable; `#!cpp std::is_integral` is in fact `#!cpp true`, but the `get_impl_ptr` overloads for `boolean_t`, `number_integer_t`, `number_unsigned_t` and `number_float_t` would collide | ## `NumberFloatType` diff --git a/include/nlohmann/detail/conversions/from_json.hpp b/include/nlohmann/detail/conversions/from_json.hpp index 0332474b2..6de9bc803 100644 --- a/include/nlohmann/detail/conversions/from_json.hpp +++ b/include/nlohmann/detail/conversions/from_json.hpp @@ -150,7 +150,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) { get_arithmetic_value(j, val); } diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index d0d9610f4..eb78f4331 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -303,14 +303,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept } template::value, int> = 0> + enable_if_t, CompatibleNumberUnsignedType>::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept { external_constructor::construct(j, static_cast(val)); } template::value, int> = 0> + enable_if_t::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept { external_constructor::construct(j, static_cast(val)); diff --git a/include/nlohmann/detail/input/number_parse.hpp b/include/nlohmann/detail/input/number_parse.hpp index 8971f7193..56a18b640 100644 --- a/include/nlohmann/detail/input/number_parse.hpp +++ b/include/nlohmann/detail/input/number_parse.hpp @@ -18,6 +18,7 @@ #include // memcpy #include // numeric_limits #include // string +#include // is_signed, true_type, false_type #include #include @@ -44,6 +45,20 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// whether an integer of a signed type is negative; the overload for other types +// avoids comparing an unsigned value with zero +template +constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept +{ + return x < 0; +} + +template +constexpr bool is_negative_integer(NumberType /*unused*/, std::false_type /*is_signed*/) noexcept +{ + return false; +} + /*! @brief fast integer parser for an already-validated unsigned integer @@ -77,8 +92,10 @@ bool parse_integer_unsigned(const char* first, const char* last, NumberUnsignedT x = (x * 10u) + digit; } value = static_cast(x); - // reject values that do not round-trip into a narrower NumberUnsignedType - return static_cast(value) == x; + // reject values that do not round-trip into a narrower NumberUnsignedType, + // and values that wrap around to negative numbers if it is a signed type + // (number_unsigned_t may be the same type as number_integer_t, #2573) + return static_cast(value) == x && !is_negative_integer(value, std::is_signed()); } /*! diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index a08ad1500..036aeb7d9 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -683,6 +683,22 @@ struct is_compatible_integer_type : is_compatible_integer_type_impl {}; +// whether to_json() stores an integer of type CompatibleNumberIntegerType as +// number_integer +template +struct is_compatible_number_integer_type + : is_compatible_integer_type {}; + +// if number_unsigned_t is the same type as number_integer_t, unsigned integers +// are stored as number_integer as well (#2573) +template +struct is_compatible_number_integer_type < BasicJsonType, CompatibleNumberIntegerType, + enable_if_t < std::is_same::value&& + std::is_integral::value&& + std::is_unsigned::value&& + !std::is_same::value >> + : std::is_constructible {}; + template struct is_compatible_type_impl: std::false_type {}; @@ -1020,6 +1036,20 @@ constexpr bool value_in_range_of(T val) template using bool_constant = std::integral_constant; +// Stands in for number_unsigned_t in overload sets if it is the same type as +// number_integer_t, so that the unsigned overloads do not collide with the +// integer ones. Never constructed. Such a basic_json stores every integer as +// number_integer (#2573). +struct same_as_number_integer_type {}; + +// number_unsigned_t of BasicJsonType, or same_as_number_integer_type if it is +// the same type as number_integer_t +template +using number_unsigned_overload_t = typename std::conditional < + std::is_same::value, + same_as_number_integer_type, + typename BasicJsonType::number_unsigned_t >::type; + /////////////////////////////////////////////////////////////////////////////// // is_c_string /////////////////////////////////////////////////////////////////////////////// diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 2a18ea600..3a9abbc08 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -221,7 +221,7 @@ class binary_writer case value_t::number_unsigned: { - write_cbor_head(0x00, j.m_data.m_value.number_unsigned); + write_cbor_head(0x00, conditional_static_cast(j.m_data.m_value.number_unsigned)); break; } @@ -1601,7 +1601,7 @@ class binary_writer return calc_bson_integer_size(j.m_data.m_value.number_integer); case value_t::number_unsigned: - return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); + return calc_bson_unsigned_size(conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return calc_bson_string_size(*j.m_data.m_value.string, j); @@ -1643,7 +1643,7 @@ class binary_writer return write_bson_integer(name, j.m_data.m_value.number_integer); case value_t::number_unsigned: - return write_bson_unsigned(name, j.m_data.m_value.number_unsigned); + return write_bson_unsigned(name, conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return write_bson_string(name, *j.m_data.m_value.string); diff --git a/include/nlohmann/detail/output/serializer.hpp b/include/nlohmann/detail/output/serializer.hpp index 64c4fcf03..8776f24ae 100644 --- a/include/nlohmann/detail/output/serializer.hpp +++ b/include/nlohmann/detail/output/serializer.hpp @@ -49,6 +49,12 @@ class serializer using number_float_t = typename BasicJsonType::number_float_t; using number_integer_t = typename BasicJsonType::number_integer_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + /// type of the absolute value of an integer: number_unsigned_t, or its + /// unsigned counterpart if it is signed (it may be number_integer_t, #2573) + using number_magnitude_t = typename std::conditional < + std::is_integral::value && std::is_signed::value, + std::make_unsigned, + std::enable_if>::type::type; using binary_char_t = typename BasicJsonType::binary_t::value_type; public: @@ -1227,7 +1233,7 @@ class serializer @param[in] x unsigned integer number to count its digits @return number of decimal digits */ - unsigned int count_digits(number_unsigned_t x) noexcept + unsigned int count_digits(number_magnitude_t x) noexcept { unsigned int n_digits = 1; for (;;) @@ -1405,7 +1411,7 @@ class serializer // use a pointer to fill the buffer auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto) - number_unsigned_t abs_value; + number_magnitude_t abs_value; unsigned int n_chars{}; @@ -1419,7 +1425,7 @@ class serializer } else { - abs_value = static_cast(x); + abs_value = static_cast(x); n_chars = count_digits(abs_value); } @@ -1562,17 +1568,6 @@ class serializer } } - /* - * Overload to make the compiler happy while it is instantiating - * dump_integer for number_unsigned_t. - * Must never be called. - */ - number_unsigned_t remove_sign(number_unsigned_t x) - { - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - return x; // LCOV_EXCL_LINE - } - /* * Helper function for dump_integer * @@ -1582,10 +1577,10 @@ class serializer * absolute values of INT_MIN and INT_MAX are usually not the same. See * #1708 for details. */ - number_unsigned_t remove_sign(number_integer_t x) noexcept + number_magnitude_t remove_sign(number_integer_t x) noexcept { JSON_ASSERT(x < 0); - return static_cast(-(x + 1)) + 1; + return static_cast(-(x + 1)) + 1; } private: diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 3bad024f1..5d6ed07c8 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -500,8 +500,22 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec "NumberUnsignedType must be at least as wide as NumberIntegerType, " "because it has to hold the absolute value of every NumberIntegerType value"); + // NumberUnsignedType may be the same type as NumberIntegerType (#2573), but + // a repeated NumberFloatType would give overloads that collide, so it gets + // a readable diagnostic instead. + static_assert(!std::is_same::value && !std::is_same::value, + "NumberFloatType must be a different type than NumberIntegerType and NumberUnsignedType"); + private: + /// the kind a value of kind @a t is stored as: if number_unsigned_t is the + /// same type as number_integer_t, every integer is stored as number_integer + /// (#2573); every other kind is stored as itself + static constexpr value_t stored_kind(const value_t t) noexcept + { + return (std::is_same::value && t == value_t::number_unsigned) ? value_t::number_integer : t; + } + /// helper for exception-safe object creation template JSON_HEDLEY_RETURNS_NON_NULL @@ -575,8 +589,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec json_value(boolean_t v) noexcept : boolean(v) {} /// constructor for numbers (integer) json_value(number_integer_t v) noexcept : number_integer(v) {} - /// constructor for numbers (unsigned) - json_value(number_unsigned_t v) noexcept : number_unsigned(v) {} + /// constructor for numbers (unsigned); takes a placeholder if number_unsigned_t is number_integer_t + json_value(detail::number_unsigned_overload_t v) noexcept : number_unsigned(v) {} /// constructor for numbers (floating-point) json_value(number_float_t v) noexcept : number_float(v) {} /// constructor for empty values of a given type @@ -2181,7 +2195,7 @@ public: /// @brief create an empty value with a given type /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const value_t v) - : m_data(v) + : m_data(stored_kind(v)) { assert_invariant(); } @@ -2262,7 +2276,7 @@ public: { convert_leaf(val); } - JSON_ASSERT(m_data.m_type == val.type()); + JSON_ASSERT(m_data.m_type == stored_kind(val.type())); set_parents(); assert_invariant(); @@ -2847,14 +2861,16 @@ public: return m_data.m_type == value_t::number_integer ? &m_data.m_value.number_integer : nullptr; } - /// get a pointer to the value (unsigned number) - number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + number_unsigned_t* get_impl_ptr(detail::number_unsigned_overload_t* /*unused*/) noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } - /// get a pointer to the value (unsigned number) - constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + constexpr const number_unsigned_t* get_impl_ptr(const detail::number_unsigned_overload_t* /*unused*/) const noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 16c87bf36..9a572f69a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4687,6 +4687,22 @@ struct is_compatible_integer_type : is_compatible_integer_type_impl {}; +// whether to_json() stores an integer of type CompatibleNumberIntegerType as +// number_integer +template +struct is_compatible_number_integer_type + : is_compatible_integer_type {}; + +// if number_unsigned_t is the same type as number_integer_t, unsigned integers +// are stored as number_integer as well (#2573) +template +struct is_compatible_number_integer_type < BasicJsonType, CompatibleNumberIntegerType, + enable_if_t < std::is_same::value&& + std::is_integral::value&& + std::is_unsigned::value&& + !std::is_same::value >> + : std::is_constructible {}; + template struct is_compatible_type_impl: std::false_type {}; @@ -5024,6 +5040,20 @@ constexpr bool value_in_range_of(T val) template using bool_constant = std::integral_constant; +// Stands in for number_unsigned_t in overload sets if it is the same type as +// number_integer_t, so that the unsigned overloads do not collide with the +// integer ones. Never constructed. Such a basic_json stores every integer as +// number_integer (#2573). +struct same_as_number_integer_type {}; + +// number_unsigned_t of BasicJsonType, or same_as_number_integer_type if it is +// the same type as number_integer_t +template +using number_unsigned_overload_t = typename std::conditional < + std::is_same::value, + same_as_number_integer_type, + typename BasicJsonType::number_unsigned_t >::type; + /////////////////////////////////////////////////////////////////////////////// // is_c_string /////////////////////////////////////////////////////////////////////////////// @@ -5846,7 +5876,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo } template -inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +inline void from_json(const BasicJsonType& j, number_unsigned_overload_t& val) { get_arithmetic_value(j, val); } @@ -7250,14 +7280,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept } template::value, int> = 0> + enable_if_t, CompatibleNumberUnsignedType>::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept { external_constructor::construct(j, static_cast(val)); } template::value, int> = 0> + enable_if_t::value, int> = 0> inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept { external_constructor::construct(j, static_cast(val)); @@ -9065,6 +9095,7 @@ NLOHMANN_JSON_NAMESPACE_END #include // memcpy #include // numeric_limits #include // string +#include // is_signed, true_type, false_type // #include @@ -9466,6 +9497,20 @@ NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { +// whether an integer of a signed type is negative; the overload for other types +// avoids comparing an unsigned value with zero +template +constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept +{ + return x < 0; +} + +template +constexpr bool is_negative_integer(NumberType /*unused*/, std::false_type /*is_signed*/) noexcept +{ + return false; +} + /*! @brief fast integer parser for an already-validated unsigned integer @@ -9499,8 +9544,10 @@ bool parse_integer_unsigned(const char* first, const char* last, NumberUnsignedT x = (x * 10u) + digit; } value = static_cast(x); - // reject values that do not round-trip into a narrower NumberUnsignedType - return static_cast(value) == x; + // reject values that do not round-trip into a narrower NumberUnsignedType, + // and values that wrap around to negative numbers if it is a signed type + // (number_unsigned_t may be the same type as number_integer_t, #2573) + return static_cast(value) == x && !is_negative_integer(value, std::is_signed()); } /*! @@ -22122,7 +22169,7 @@ class binary_writer case value_t::number_unsigned: { - write_cbor_head(0x00, j.m_data.m_value.number_unsigned); + write_cbor_head(0x00, conditional_static_cast(j.m_data.m_value.number_unsigned)); break; } @@ -23502,7 +23549,7 @@ class binary_writer return calc_bson_integer_size(j.m_data.m_value.number_integer); case value_t::number_unsigned: - return calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); + return calc_bson_unsigned_size(conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return calc_bson_string_size(*j.m_data.m_value.string, j); @@ -23544,7 +23591,7 @@ class binary_writer return write_bson_integer(name, j.m_data.m_value.number_integer); case value_t::number_unsigned: - return write_bson_unsigned(name, j.m_data.m_value.number_unsigned); + return write_bson_unsigned(name, conditional_static_cast(j.m_data.m_value.number_unsigned)); case value_t::string: return write_bson_string(name, *j.m_data.m_value.string); @@ -26364,6 +26411,12 @@ class serializer using number_float_t = typename BasicJsonType::number_float_t; using number_integer_t = typename BasicJsonType::number_integer_t; using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + /// type of the absolute value of an integer: number_unsigned_t, or its + /// unsigned counterpart if it is signed (it may be number_integer_t, #2573) + using number_magnitude_t = typename std::conditional < + std::is_integral::value && std::is_signed::value, + std::make_unsigned, + std::enable_if>::type::type; using binary_char_t = typename BasicJsonType::binary_t::value_type; public: @@ -27542,7 +27595,7 @@ class serializer @param[in] x unsigned integer number to count its digits @return number of decimal digits */ - unsigned int count_digits(number_unsigned_t x) noexcept + unsigned int count_digits(number_magnitude_t x) noexcept { unsigned int n_digits = 1; for (;;) @@ -27720,7 +27773,7 @@ class serializer // use a pointer to fill the buffer auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto) - number_unsigned_t abs_value; + number_magnitude_t abs_value; unsigned int n_chars{}; @@ -27734,7 +27787,7 @@ class serializer } else { - abs_value = static_cast(x); + abs_value = static_cast(x); n_chars = count_digits(abs_value); } @@ -27877,17 +27930,6 @@ class serializer } } - /* - * Overload to make the compiler happy while it is instantiating - * dump_integer for number_unsigned_t. - * Must never be called. - */ - number_unsigned_t remove_sign(number_unsigned_t x) - { - JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE - return x; // LCOV_EXCL_LINE - } - /* * Helper function for dump_integer * @@ -27897,10 +27939,10 @@ class serializer * absolute values of INT_MIN and INT_MAX are usually not the same. See * #1708 for details. */ - number_unsigned_t remove_sign(number_integer_t x) noexcept + number_magnitude_t remove_sign(number_integer_t x) noexcept { JSON_ASSERT(x < 0); - return static_cast(-(x + 1)) + 1; + return static_cast(-(x + 1)) + 1; } private: @@ -28784,8 +28826,22 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec "NumberUnsignedType must be at least as wide as NumberIntegerType, " "because it has to hold the absolute value of every NumberIntegerType value"); + // NumberUnsignedType may be the same type as NumberIntegerType (#2573), but + // a repeated NumberFloatType would give overloads that collide, so it gets + // a readable diagnostic instead. + static_assert(!std::is_same::value && !std::is_same::value, + "NumberFloatType must be a different type than NumberIntegerType and NumberUnsignedType"); + private: + /// the kind a value of kind @a t is stored as: if number_unsigned_t is the + /// same type as number_integer_t, every integer is stored as number_integer + /// (#2573); every other kind is stored as itself + static constexpr value_t stored_kind(const value_t t) noexcept + { + return (std::is_same::value && t == value_t::number_unsigned) ? value_t::number_integer : t; + } + /// helper for exception-safe object creation template JSON_HEDLEY_RETURNS_NON_NULL @@ -28859,8 +28915,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec json_value(boolean_t v) noexcept : boolean(v) {} /// constructor for numbers (integer) json_value(number_integer_t v) noexcept : number_integer(v) {} - /// constructor for numbers (unsigned) - json_value(number_unsigned_t v) noexcept : number_unsigned(v) {} + /// constructor for numbers (unsigned); takes a placeholder if number_unsigned_t is number_integer_t + json_value(detail::number_unsigned_overload_t v) noexcept : number_unsigned(v) {} /// constructor for numbers (floating-point) json_value(number_float_t v) noexcept : number_float(v) {} /// constructor for empty values of a given type @@ -30465,7 +30521,7 @@ public: /// @brief create an empty value with a given type /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const value_t v) - : m_data(v) + : m_data(stored_kind(v)) { assert_invariant(); } @@ -30546,7 +30602,7 @@ public: { convert_leaf(val); } - JSON_ASSERT(m_data.m_type == val.type()); + JSON_ASSERT(m_data.m_type == stored_kind(val.type())); set_parents(); assert_invariant(); @@ -31131,14 +31187,16 @@ public: return m_data.m_type == value_t::number_integer ? &m_data.m_value.number_integer : nullptr; } - /// get a pointer to the value (unsigned number) - number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + number_unsigned_t* get_impl_ptr(detail::number_unsigned_overload_t* /*unused*/) noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } - /// get a pointer to the value (unsigned number) - constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept + /// get a pointer to the value (unsigned number); takes a placeholder if + /// number_unsigned_t is number_integer_t (the integer overload handles it then) + constexpr const number_unsigned_t* get_impl_ptr(const detail::number_unsigned_overload_t* /*unused*/) const noexcept { return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } diff --git a/tests/src/unit-same-number-types.cpp b/tests/src/unit-same-number-types.cpp new file mode 100644 index 000000000..f2c501294 --- /dev/null +++ b/tests/src/unit-same-number-types.cpp @@ -0,0 +1,202 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include +#include + +// basic_json with the same type for number_integer_t and number_unsigned_t +// (#2573): every integer is stored as number_integer + +namespace +{ + +template +using same_types_json = nlohmann::basic_json; + +using json64 = same_types_json; +using json32 = same_types_json; + +} // namespace + +TEST_CASE_TEMPLATE("same type for number_integer_t and number_unsigned_t", Json, json64, json32) +{ + using integer_t = typename Json::number_integer_t; + using limits = std::numeric_limits; + + SECTION("parsing stores integers as number_integer") + { + const Json j = Json::parse("[0, 1, -1, 2147483647, -2147483648]"); + for (const auto& element : j) + { + CHECK(element.is_number_integer()); + CHECK(!element.is_number_unsigned()); + CHECK(element.type() == nlohmann::detail::value_t::number_integer); + } + CHECK(j[1].template get() == 1); + CHECK(j[2].template get() == -1); + CHECK(j[3].template get() == 2147483647); + CHECK(j[4].template get() == -2147483648LL); + } + + SECTION("integers above the maximum are parsed as floats") + { + // a positive literal is parsed as unsigned first; it must not wrap + // around to a negative number_integer_t + const auto above_max = std::to_string(static_cast(limits::max()) + 1); + const Json j1 = Json::parse(above_max); + CHECK(j1.is_number_float()); + CHECK(j1.template get() == static_cast(static_cast(limits::max()) + 1)); + + const Json j2 = Json::parse("18446744073709551615"); + CHECK(j2.is_number_float()); + CHECK(j2.template get() == 18446744073709551615.0); + + const Json j3 = Json::parse(std::to_string(limits::max())); + CHECK(j3.is_number_integer()); + CHECK(j3.template get() == limits::max()); + } + + SECTION("serialization of the extremes") + { + CHECK(Json(limits::max()).dump() == std::to_string(limits::max())); + // the absolute value of the minimum does not fit into number_unsigned_t + CHECK(Json(limits::min()).dump() == std::to_string(limits::min())); + CHECK(Json::parse(std::to_string(limits::min())) == Json(limits::min())); + CHECK(Json(0).dump() == "0"); + } + + SECTION("construction from unsigned types") + { + const Json j1 = 5u; + CHECK(j1.is_number_integer()); + CHECK(!j1.is_number_unsigned()); + CHECK(j1 == 5); + + const Json j2 = static_cast(200); + CHECK(j2.is_number_integer()); + CHECK(j2.template get() == 200); + + const Json j3 = std::vector {1, 2, 3}; + CHECK(j3.dump() == "[1,2,3]"); + CHECK(j3[0].is_number_integer()); + + const Json j4 = true; + CHECK(j4.is_boolean()); + } + + SECTION("value_t::number_unsigned creates a number_integer") + { + const Json j(nlohmann::detail::value_t::number_unsigned); + CHECK(j.is_number_integer()); + CHECK(!j.is_number_unsigned()); + CHECK(j == 0); + CHECK(j.dump() == "0"); + } + + SECTION("conversions") + { + Json j = 42; + CHECK(j.template get() == 42u); + CHECK(j.template get() == 42u); + CHECK(j.template get() == 42); + CHECK(j.template get() == 42.0); + CHECK(j.template get_ptr() != nullptr); + CHECK(*j.template get_ptr() == 42); + CHECK(j.template get_ref() == 42); + j.template get_ref() = 43; + CHECK(j == 43); + + integer_t i = 0; + j.get_to(i); + CHECK(i == 43); + + const Json object = {{"a", 1}}; + CHECK(object.value("a", 0u) == 1u); + CHECK(object.value("b", 7u) == 7u); + } + + SECTION("conversion from a basic_json with distinct types") + { + const nlohmann::json j = {1, 2u, -3, 4.5}; + CHECK(j[1].is_number_unsigned()); + const Json converted = j; + CHECK(converted.dump() == "[1,2,-3,4.5]"); + CHECK(converted[1].is_number_integer()); + CHECK(!converted[1].is_number_unsigned()); + + const nlohmann::json back = converted; + CHECK(back == j); + } + + SECTION("comparison and hashing") + { + const Json j1 = 1; + const Json j2 = 2u; + CHECK(j1 < j2); + CHECK(j1 != j2); + CHECK(j2 == 2); + CHECK(j2 == 2u); + CHECK(j1 < 1.5); + CHECK(Json(-1) < Json(0u)); + CHECK(std::hash {}(Json(5u)) == std::hash {}(Json(5))); + } + + SECTION("JSON Patch, diff, and flatten") + { + const Json source = Json::parse(R"({"a": [1, 2, 3], "b": 4})"); + const Json target = Json::parse(R"({"a": [1, 5], "c": 6})"); + CHECK(source.patch(Json::diff(source, target)) == target); + CHECK(source.flatten().unflatten() == source); + } + + SECTION("binary formats") + { + const Json j = {{"min", limits::min()}, {"max", limits::max()}, {"zero", 0}, {"small", 100}, {"negative", -100}, {"float", 1.5}}; + + CHECK(Json::from_cbor(Json::to_cbor(j)) == j); + CHECK(Json::from_msgpack(Json::to_msgpack(j)) == j); + CHECK(Json::from_ubjson(Json::to_ubjson(j)) == j); + CHECK(Json::from_bjdata(Json::to_bjdata(j)) == j); + CHECK(Json::from_bson(Json::to_bson(j)) == j); + CHECK(Json::from_bon8(Json::to_bon8(j)) == j); + + for (const auto& element : Json::from_cbor(Json::to_cbor(j))) + { + CHECK(!element.is_number_unsigned()); + } + + // CBOR unsigned integer 2^64-1 does not fit and is read as float + const std::vector cbor_max = {0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const Json from_cbor = Json::from_cbor(cbor_max); + CHECK(from_cbor.is_number_float()); + CHECK(from_cbor.template get() == 18446744073709551615.0); + + // MessagePack uint 8 is read as number_integer + const std::vector msgpack_uint8 = {0xCC, 0xC8}; + const Json from_msgpack = Json::from_msgpack(msgpack_uint8); + CHECK(from_msgpack.is_number_integer()); + CHECK(from_msgpack == 200); + } +} + +TEST_CASE("same type for number_integer_t and number_unsigned_t: type traits") +{ + CHECK(std::is_same, nlohmann::detail::same_as_number_integer_type>::value); + CHECK(std::is_same, std::uint64_t>::value); + CHECK(nlohmann::detail::is_compatible_number_integer_type::value); + CHECK(!nlohmann::detail::is_compatible_number_integer_type::value); + CHECK(!nlohmann::detail::is_compatible_number_integer_type::value); +}