mirror of
https://github.com/nlohmann/json.git
synced 2026-07-10 12:35:09 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10d18792bd | |||
| 8557b661ab | |||
| 48face7a20 | |||
| 68c87ad9de |
@@ -6,9 +6,18 @@ namespace std {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Return a hash value for a JSON object. The hash function tries to rely on `std::hash` where possible. Furthermore, the
|
Return a hash value for a JSON object. The hash function tries to rely on `std::hash` where possible. To satisfy the
|
||||||
type of the JSON value is taken into account to have different hash values for `#!json null`, `#!cpp 0`, `#!cpp 0U`, and
|
`std::hash` contract, numeric JSON values that compare equal must hash to the same value. This means:
|
||||||
`#!cpp false`, etc.
|
|
||||||
|
- `json(42)`, `json(42u)`, and `json(42.0)` all hash to the same value
|
||||||
|
- `json(0)`, `json(0u)`, and `json(0.0)` all hash to the same value
|
||||||
|
|
||||||
|
Different types hash differently for non-numeric types (e.g., `#!json null`, `#!cpp false`, and strings all have distinct hashes).
|
||||||
|
|
||||||
|
**Edge case:** For very large integers outside the exact representable range of the floating-point type (beyond ~2^53 for
|
||||||
|
typical `double`), the hash values for integer and floating-point values may differ, even if the floating-point value
|
||||||
|
was obtained by casting the integer (due to precision loss). This is a documented limitation arising from how the
|
||||||
|
comparison operator normalizes numeric types.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ int main()
|
|||||||
<< "hash(false) = " << std::hash<json> {}(json(false)) << '\n'
|
<< "hash(false) = " << std::hash<json> {}(json(false)) << '\n'
|
||||||
<< "hash(0) = " << std::hash<json> {}(json(0)) << '\n'
|
<< "hash(0) = " << std::hash<json> {}(json(0)) << '\n'
|
||||||
<< "hash(0U) = " << std::hash<json> {}(json(0U)) << '\n'
|
<< "hash(0U) = " << std::hash<json> {}(json(0U)) << '\n'
|
||||||
|
<< "hash(0.0) = " << std::hash<json> {}(json(0.0)) << '\n'
|
||||||
<< "hash(\"\") = " << std::hash<json> {}(json("")) << '\n'
|
<< "hash(\"\") = " << std::hash<json> {}(json("")) << '\n'
|
||||||
<< "hash({}) = " << std::hash<json> {}(json::object()) << '\n'
|
<< "hash({}) = " << std::hash<json> {}(json::object()) << '\n'
|
||||||
<< "hash([]) = " << std::hash<json> {}(json::array()) << '\n'
|
<< "hash([]) = " << std::hash<json> {}(json::array()) << '\n'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
hash(null) = 2654435769
|
hash(null) = 2654435769
|
||||||
hash(false) = 2654436030
|
hash(false) = 2654436030
|
||||||
hash(0) = 2654436095
|
hash(0) = 2654436221
|
||||||
hash(0U) = 2654436156
|
hash(0U) = 2654436221
|
||||||
hash("") = 6142509191626859748
|
hash(0.0) = 2654436221
|
||||||
|
hash("") = 11160318156688833227
|
||||||
hash({}) = 2654435832
|
hash({}) = 2654435832
|
||||||
hash([]) = 2654435899
|
hash([]) = 2654435899
|
||||||
hash({"hello": "world"}) = 4469488738203676328
|
hash({"hello": "world"}) = 3701319991624763853
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <cstdint> // uint8_t
|
#include <cstdint> // uint8_t
|
||||||
#include <cstddef> // size_t
|
#include <cstddef> // size_t
|
||||||
#include <functional> // hash
|
#include <functional> // hash
|
||||||
|
#include <limits> // numeric_limits
|
||||||
|
#include <cmath> // isfinite
|
||||||
|
|
||||||
#include <nlohmann/detail/abi_macros.hpp>
|
#include <nlohmann/detail/abi_macros.hpp>
|
||||||
#include <nlohmann/detail/value_t.hpp>
|
#include <nlohmann/detail/value_t.hpp>
|
||||||
@@ -26,12 +28,73 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
|
|||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a number_integer_t value is exactly representable as number_float_t
|
||||||
|
// Returns true if static_cast<number_integer_t>(static_cast<number_float_t>(val)) == val
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
inline bool is_exactly_representable_as_float(typename BasicJsonType::number_integer_t val) noexcept
|
||||||
|
{
|
||||||
|
using number_integer_t = typename BasicJsonType::number_integer_t;
|
||||||
|
using number_float_t = typename BasicJsonType::number_float_t;
|
||||||
|
|
||||||
|
// If the float type's mantissa covers the integer type's entire range, all values round-trip
|
||||||
|
constexpr int float_digits = std::numeric_limits<number_float_t>::digits;
|
||||||
|
constexpr int int_digits = std::numeric_limits<number_integer_t>::digits;
|
||||||
|
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning(push )
|
||||||
|
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
|
||||||
|
#endif
|
||||||
|
if (float_digits >= int_digits)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// For values outside float's exact range, they don't round-trip
|
||||||
|
// The safe way to check: compute the max magnitude that round-trips
|
||||||
|
// Using unsigned arithmetic to avoid UB with negating INT_MIN
|
||||||
|
|
||||||
|
// Max magnitude representable exactly: 2^(digits-1) - 1 for signed, 2^digits - 1 for unsigned range
|
||||||
|
// But we're checking a signed value, so use 2^digits as the threshold
|
||||||
|
constexpr auto max_exact = static_cast<number_integer_t>(1) << (float_digits - 1);
|
||||||
|
|
||||||
|
// Check absolute value against this threshold
|
||||||
|
if (val >= 0)
|
||||||
|
{
|
||||||
|
if (val >= max_exact)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For negative values, check via unsigned wrapping arithmetic
|
||||||
|
// -val in unsigned domain; if it wraps, the value is too negative
|
||||||
|
auto unsigned_abs = static_cast<typename BasicJsonType::number_unsigned_t>(-val);
|
||||||
|
if (unsigned_abs >= static_cast<typename BasicJsonType::number_unsigned_t>(max_exact))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For values within the exact range, verify the round-trip
|
||||||
|
const auto f = static_cast<number_float_t>(val);
|
||||||
|
return std::isfinite(f) && static_cast<number_integer_t>(f) == val;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief hash a JSON value
|
@brief hash a JSON value
|
||||||
|
|
||||||
The hash function tries to rely on std::hash where possible. Furthermore, the
|
The hash function tries to rely on std::hash where possible. Furthermore, the
|
||||||
type of the JSON value is taken into account to have different hash values for
|
type of the JSON value is taken into account to have different hash values for
|
||||||
null, 0, 0U, and false, etc.
|
most types. However, numeric types (number_integer, number_unsigned, number_float)
|
||||||
|
are hashed to satisfy the std::hash contract: if two json values compare equal,
|
||||||
|
they must have equal hash values. This means json(42), json(42u), and json(42.0)
|
||||||
|
all hash to the same value (since they compare equal). For large integer values
|
||||||
|
outside the exact representable range of the float type, integer values are hashed
|
||||||
|
in their own domain to avoid precision loss.
|
||||||
|
|
||||||
@tparam BasicJsonType basic_json specialization
|
@tparam BasicJsonType basic_json specialization
|
||||||
@param j JSON value to hash
|
@param j JSON value to hash
|
||||||
@@ -90,14 +153,36 @@ std::size_t hash(const BasicJsonType& j)
|
|||||||
|
|
||||||
case BasicJsonType::value_t::number_integer:
|
case BasicJsonType::value_t::number_integer:
|
||||||
{
|
{
|
||||||
const auto h = std::hash<number_integer_t> {}(j.template get<number_integer_t>());
|
const auto v = j.template get<number_integer_t>();
|
||||||
return combine(type, h);
|
// Use a shared numeric type tag so all numeric types that are equal hash the same
|
||||||
|
const auto numeric_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
|
||||||
|
|
||||||
|
if (is_exactly_representable_as_float<BasicJsonType>(v))
|
||||||
|
{
|
||||||
|
const auto h = std::hash<number_float_t> {}(static_cast<number_float_t>(v));
|
||||||
|
return combine(numeric_type, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto h = std::hash<number_integer_t> {}(v);
|
||||||
|
return combine(numeric_type, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
case BasicJsonType::value_t::number_unsigned:
|
case BasicJsonType::value_t::number_unsigned:
|
||||||
{
|
{
|
||||||
const auto h = std::hash<number_unsigned_t> {}(j.template get<number_unsigned_t>());
|
const auto v = j.template get<number_unsigned_t>();
|
||||||
return combine(type, h);
|
// Normalize to signed (matching operator== behavior for U-vs-I comparison)
|
||||||
|
const auto v_as_signed = static_cast<number_integer_t>(v);
|
||||||
|
// Use a shared numeric type tag so all numeric types that are equal hash the same
|
||||||
|
const auto numeric_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
|
||||||
|
|
||||||
|
if (is_exactly_representable_as_float<BasicJsonType>(v_as_signed))
|
||||||
|
{
|
||||||
|
const auto h = std::hash<number_float_t> {}(static_cast<number_float_t>(v_as_signed));
|
||||||
|
return combine(numeric_type, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto h = std::hash<number_integer_t> {}(v_as_signed);
|
||||||
|
return combine(numeric_type, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
case BasicJsonType::value_t::number_float:
|
case BasicJsonType::value_t::number_float:
|
||||||
|
|||||||
@@ -6677,6 +6677,8 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
#include <cstdint> // uint8_t
|
#include <cstdint> // uint8_t
|
||||||
#include <cstddef> // size_t
|
#include <cstddef> // size_t
|
||||||
#include <functional> // hash
|
#include <functional> // hash
|
||||||
|
#include <limits> // numeric_limits
|
||||||
|
#include <cmath> // isfinite
|
||||||
|
|
||||||
// #include <nlohmann/detail/abi_macros.hpp>
|
// #include <nlohmann/detail/abi_macros.hpp>
|
||||||
|
|
||||||
@@ -6694,12 +6696,73 @@ inline std::size_t combine(std::size_t seed, std::size_t h) noexcept
|
|||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a number_integer_t value is exactly representable as number_float_t
|
||||||
|
// Returns true if static_cast<number_integer_t>(static_cast<number_float_t>(val)) == val
|
||||||
|
template<typename BasicJsonType>
|
||||||
|
inline bool is_exactly_representable_as_float(typename BasicJsonType::number_integer_t val) noexcept
|
||||||
|
{
|
||||||
|
using number_integer_t = typename BasicJsonType::number_integer_t;
|
||||||
|
using number_float_t = typename BasicJsonType::number_float_t;
|
||||||
|
|
||||||
|
// If the float type's mantissa covers the integer type's entire range, all values round-trip
|
||||||
|
constexpr int float_digits = std::numeric_limits<number_float_t>::digits;
|
||||||
|
constexpr int int_digits = std::numeric_limits<number_integer_t>::digits;
|
||||||
|
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning(push )
|
||||||
|
#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr
|
||||||
|
#endif
|
||||||
|
if (float_digits >= int_digits)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#ifdef JSON_HEDLEY_MSVC_VERSION
|
||||||
|
#pragma warning( pop )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// For values outside float's exact range, they don't round-trip
|
||||||
|
// The safe way to check: compute the max magnitude that round-trips
|
||||||
|
// Using unsigned arithmetic to avoid UB with negating INT_MIN
|
||||||
|
|
||||||
|
// Max magnitude representable exactly: 2^(digits-1) - 1 for signed, 2^digits - 1 for unsigned range
|
||||||
|
// But we're checking a signed value, so use 2^digits as the threshold
|
||||||
|
constexpr auto max_exact = static_cast<number_integer_t>(1) << (float_digits - 1);
|
||||||
|
|
||||||
|
// Check absolute value against this threshold
|
||||||
|
if (val >= 0)
|
||||||
|
{
|
||||||
|
if (val >= max_exact)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For negative values, check via unsigned wrapping arithmetic
|
||||||
|
// -val in unsigned domain; if it wraps, the value is too negative
|
||||||
|
auto unsigned_abs = static_cast<typename BasicJsonType::number_unsigned_t>(-val);
|
||||||
|
if (unsigned_abs >= static_cast<typename BasicJsonType::number_unsigned_t>(max_exact))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For values within the exact range, verify the round-trip
|
||||||
|
const auto f = static_cast<number_float_t>(val);
|
||||||
|
return std::isfinite(f) && static_cast<number_integer_t>(f) == val;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief hash a JSON value
|
@brief hash a JSON value
|
||||||
|
|
||||||
The hash function tries to rely on std::hash where possible. Furthermore, the
|
The hash function tries to rely on std::hash where possible. Furthermore, the
|
||||||
type of the JSON value is taken into account to have different hash values for
|
type of the JSON value is taken into account to have different hash values for
|
||||||
null, 0, 0U, and false, etc.
|
most types. However, numeric types (number_integer, number_unsigned, number_float)
|
||||||
|
are hashed to satisfy the std::hash contract: if two json values compare equal,
|
||||||
|
they must have equal hash values. This means json(42), json(42u), and json(42.0)
|
||||||
|
all hash to the same value (since they compare equal). For large integer values
|
||||||
|
outside the exact representable range of the float type, integer values are hashed
|
||||||
|
in their own domain to avoid precision loss.
|
||||||
|
|
||||||
@tparam BasicJsonType basic_json specialization
|
@tparam BasicJsonType basic_json specialization
|
||||||
@param j JSON value to hash
|
@param j JSON value to hash
|
||||||
@@ -6758,14 +6821,36 @@ std::size_t hash(const BasicJsonType& j)
|
|||||||
|
|
||||||
case BasicJsonType::value_t::number_integer:
|
case BasicJsonType::value_t::number_integer:
|
||||||
{
|
{
|
||||||
const auto h = std::hash<number_integer_t> {}(j.template get<number_integer_t>());
|
const auto v = j.template get<number_integer_t>();
|
||||||
return combine(type, h);
|
// Use a shared numeric type tag so all numeric types that are equal hash the same
|
||||||
|
const auto numeric_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
|
||||||
|
|
||||||
|
if (is_exactly_representable_as_float<BasicJsonType>(v))
|
||||||
|
{
|
||||||
|
const auto h = std::hash<number_float_t> {}(static_cast<number_float_t>(v));
|
||||||
|
return combine(numeric_type, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto h = std::hash<number_integer_t> {}(v);
|
||||||
|
return combine(numeric_type, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
case BasicJsonType::value_t::number_unsigned:
|
case BasicJsonType::value_t::number_unsigned:
|
||||||
{
|
{
|
||||||
const auto h = std::hash<number_unsigned_t> {}(j.template get<number_unsigned_t>());
|
const auto v = j.template get<number_unsigned_t>();
|
||||||
return combine(type, h);
|
// Normalize to signed (matching operator== behavior for U-vs-I comparison)
|
||||||
|
const auto v_as_signed = static_cast<number_integer_t>(v);
|
||||||
|
// Use a shared numeric type tag so all numeric types that are equal hash the same
|
||||||
|
const auto numeric_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
|
||||||
|
|
||||||
|
if (is_exactly_representable_as_float<BasicJsonType>(v_as_signed))
|
||||||
|
{
|
||||||
|
const auto h = std::hash<number_float_t> {}(static_cast<number_float_t>(v_as_signed));
|
||||||
|
return combine(numeric_type, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto h = std::hash<number_integer_t> {}(v_as_signed);
|
||||||
|
return combine(numeric_type, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
case BasicJsonType::value_t::number_float:
|
case BasicJsonType::value_t::number_float:
|
||||||
|
|||||||
+22
-6
@@ -35,10 +35,10 @@ TEST_CASE("hash<nlohmann::json>")
|
|||||||
|
|
||||||
// number
|
// number
|
||||||
hashes.insert(std::hash<json> {}(json(0)));
|
hashes.insert(std::hash<json> {}(json(0)));
|
||||||
hashes.insert(std::hash<json> {}(json(static_cast<unsigned>(0))));
|
hashes.insert(std::hash<json> {}(json(static_cast<unsigned>(0)))); // now same hash as json(0)
|
||||||
|
hashes.insert(std::hash<json> {}(json(0.0))); // now same hash as json(0)
|
||||||
|
|
||||||
hashes.insert(std::hash<json> {}(json(-1)));
|
hashes.insert(std::hash<json> {}(json(-1)));
|
||||||
hashes.insert(std::hash<json> {}(json(0.0)));
|
|
||||||
hashes.insert(std::hash<json> {}(json(42.23)));
|
hashes.insert(std::hash<json> {}(json(42.23)));
|
||||||
|
|
||||||
// array
|
// array
|
||||||
@@ -60,7 +60,16 @@ TEST_CASE("hash<nlohmann::json>")
|
|||||||
// discarded
|
// discarded
|
||||||
hashes.insert(std::hash<json> {}(json(json::value_t::discarded)));
|
hashes.insert(std::hash<json> {}(json(json::value_t::discarded)));
|
||||||
|
|
||||||
CHECK(hashes.size() == 21);
|
// Note: json(0), json(0U), and json(0.0) now hash to the same value
|
||||||
|
// (to satisfy the std::hash contract: equal values must hash equally)
|
||||||
|
// So we expect 19 distinct hashes instead of 21
|
||||||
|
CHECK(hashes.size() == 19);
|
||||||
|
|
||||||
|
// Verify the std::hash contract: equal values must hash equally
|
||||||
|
CHECK(std::hash<json> {}(json(0)) == std::hash<json> {}(json(static_cast<unsigned>(0))));
|
||||||
|
CHECK(std::hash<json> {}(json(0)) == std::hash<json> {}(json(0.0)));
|
||||||
|
CHECK(std::hash<json> {}(json(42)) == std::hash<json> {}(json(42u)));
|
||||||
|
CHECK(std::hash<json> {}(json(42)) == std::hash<json> {}(json(42.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("hash<nlohmann::ordered_json>")
|
TEST_CASE("hash<nlohmann::ordered_json>")
|
||||||
@@ -84,10 +93,10 @@ TEST_CASE("hash<nlohmann::ordered_json>")
|
|||||||
|
|
||||||
// number
|
// number
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(0)));
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(0)));
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(static_cast<unsigned>(0))));
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(static_cast<unsigned>(0)))); // now same hash as ordered_json(0)
|
||||||
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(0.0))); // now same hash as ordered_json(0)
|
||||||
|
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(-1)));
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(-1)));
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(0.0)));
|
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(42.23)));
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(42.23)));
|
||||||
|
|
||||||
// array
|
// array
|
||||||
@@ -109,5 +118,12 @@ TEST_CASE("hash<nlohmann::ordered_json>")
|
|||||||
// discarded
|
// discarded
|
||||||
hashes.insert(std::hash<ordered_json> {}(ordered_json(ordered_json::value_t::discarded)));
|
hashes.insert(std::hash<ordered_json> {}(ordered_json(ordered_json::value_t::discarded)));
|
||||||
|
|
||||||
CHECK(hashes.size() == 21);
|
// Note: ordered_json(0), ordered_json(0U), and ordered_json(0.0) now hash to the same value
|
||||||
|
CHECK(hashes.size() == 19);
|
||||||
|
|
||||||
|
// Verify the std::hash contract for ordered_json as well
|
||||||
|
CHECK(std::hash<ordered_json> {}(ordered_json(0)) == std::hash<ordered_json> {}(ordered_json(static_cast<unsigned>(0))));
|
||||||
|
CHECK(std::hash<ordered_json> {}(ordered_json(0)) == std::hash<ordered_json> {}(ordered_json(0.0)));
|
||||||
|
CHECK(std::hash<ordered_json> {}(ordered_json(42)) == std::hash<ordered_json> {}(ordered_json(42u)));
|
||||||
|
CHECK(std::hash<ordered_json> {}(ordered_json(42)) == std::hash<ordered_json> {}(ordered_json(42.0)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user