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 <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-13 18:57:07 +02:00
1 parent e839bbd1c0
commit 45b837885e
13 files changed
+430 -85

No files matched your search

@@ -150,7 +150,7 @@ inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_flo
}
template<typename BasicJsonType>
inline void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val)
inline void from_json(const BasicJsonType& j, number_unsigned_overload_t<BasicJsonType>& val)
{
get_arithmetic_value(j, val);
}
@@ -303,14 +303,14 @@ inline void to_json(BasicJsonType& j, FloatType val) noexcept
}
template<typename BasicJsonType, typename CompatibleNumberUnsignedType,
enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_unsigned_t, CompatibleNumberUnsignedType>::value, int> = 0>
enable_if_t<is_compatible_integer_type<number_unsigned_overload_t<BasicJsonType>, CompatibleNumberUnsignedType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept
{
external_constructor<value_t::number_unsigned>::construct(j, static_cast<typename BasicJsonType::number_unsigned_t>(val));
}
template<typename BasicJsonType, typename CompatibleNumberIntegerType,
enable_if_t<is_compatible_integer_type<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType>::value, int> = 0>
enable_if_t<is_compatible_number_integer_type<BasicJsonType, CompatibleNumberIntegerType>::value, int> = 0>
inline void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept
{
external_constructor<value_t::number_integer>::construct(j, static_cast<typename BasicJsonType::number_integer_t>(val));
+19 -2
View File
@@ -18,6 +18,7 @@
#include <cstring> // memcpy
#include <limits> // numeric_limits
#include <string> // string
#include <type_traits> // is_signed, true_type, false_type
#include <nlohmann/detail/bit_ops.hpp>
#include <nlohmann/detail/input/pow5_table.hpp>
@@ -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<typename NumberType>
constexpr bool is_negative_integer(NumberType x, std::true_type /*is_signed*/) noexcept
{
return x < 0;
}
template<typename NumberType>
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<NumberUnsignedType>(x);
// reject values that do not round-trip into a narrower NumberUnsignedType
return static_cast<std::uint64_t>(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<std::uint64_t>(value) == x && !is_negative_integer(value, std::is_signed<NumberUnsignedType>());
}
/*!
@@ -683,6 +683,22 @@ struct is_compatible_integer_type
: is_compatible_integer_type_impl<RealIntegerType,
CompatibleNumberIntegerType> {};
// whether to_json() stores an integer of type CompatibleNumberIntegerType as
// number_integer
template<typename BasicJsonType, typename CompatibleNumberIntegerType, typename = void>
struct is_compatible_number_integer_type
: is_compatible_integer_type<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType> {};
// if number_unsigned_t is the same type as number_integer_t, unsigned integers
// are stored as number_integer as well (#2573)
template<typename BasicJsonType, typename CompatibleNumberIntegerType>
struct is_compatible_number_integer_type < BasicJsonType, CompatibleNumberIntegerType,
enable_if_t < std::is_same<typename BasicJsonType::number_integer_t, typename BasicJsonType::number_unsigned_t>::value&&
std::is_integral<CompatibleNumberIntegerType>::value&&
std::is_unsigned<CompatibleNumberIntegerType>::value&&
!std::is_same<bool, CompatibleNumberIntegerType>::value >>
: std::is_constructible<typename BasicJsonType::number_integer_t, CompatibleNumberIntegerType> {};
template<typename BasicJsonType, typename CompatibleType, typename = void>
struct is_compatible_type_impl: std::false_type {};
@@ -1020,6 +1036,20 @@ constexpr bool value_in_range_of(T val)
template<bool Value>
using bool_constant = std::integral_constant<bool, Value>;
// 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<typename BasicJsonType>
using number_unsigned_overload_t = typename std::conditional <
std::is_same<typename BasicJsonType::number_integer_t, typename BasicJsonType::number_unsigned_t>::value,
same_as_number_integer_type,
typename BasicJsonType::number_unsigned_t >::type;
///////////////////////////////////////////////////////////////////////////////
// is_c_string
///////////////////////////////////////////////////////////////////////////////
@@ -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<std::uint64_t>(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<std::uint64_t>(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<std::uint64_t>(j.m_data.m_value.number_unsigned));
case value_t::string:
return write_bson_string(name, *j.m_data.m_value.string);
+11 -16
View File
@@ -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<number_unsigned_t>::value && std::is_signed<number_unsigned_t>::value,
std::make_unsigned<number_unsigned_t>,
std::enable_if<true, number_unsigned_t >>::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<number_unsigned_t>(x);
abs_value = static_cast<number_magnitude_t>(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<number_unsigned_t>(-(x + 1)) + 1;
return static_cast<number_magnitude_t>(-(x + 1)) + 1;
}
private: