Normalize -0.0 in number hashes and test range ends

operator== compares numbers exactly since #5459, so equal numbers
share one value and convert to the same number_float_t. Update the
comment accordingly, map -0.0 to 0.0 before hashing (std::hash need
not do that), and test -0.0 and the ends of the integer ranges. Show
hash(0.0) in the docs example and note the change in the version
history.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-06 22:51:44 +02:00
1 parent 071f79b524
commit 268ddeb2f0
6 files changed
+39 -14

No files matched your search

@@ -37,3 +37,5 @@ whether they are stored as `#!cpp 0`, `#!cpp 0U`, or `#!cpp 0.0`.
- Added in version 1.0.0.
- Extended for arbitrary basic_json types in version 3.10.5.
- Numbers that compare equal hash equally since version 3.13.0; before, `#!cpp 0`, `#!cpp 0U`, and `#!cpp 0.0` had
different hash values.
+1
View File
@@ -11,6 +11,7 @@ int main()
<< "hash(false) = " << std::hash<json> {}(json(false)) << '\n'
<< "hash(0) = " << std::hash<json> {}(json(0)) << '\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::object()) << '\n'
<< "hash([]) = " << std::hash<json> {}(json::array()) << '\n'
@@ -2,6 +2,7 @@ hash(null) = 2654435769
hash(false) = 2654436030
hash(0) = 2654436221
hash(0U) = 2654436221
hash(0.0) = 2654436221
hash("") = 11160318156688833227
hash({}) = 2654435832
hash([]) = 2654435899
+9 -7
View File
@@ -116,14 +116,16 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::number_unsigned:
case BasicJsonType::value_t::number_float:
{
// operator== converts between number_integer, number_unsigned, and
// number_float before comparing, so equal numbers of different
// internal types (0, 0U, 0.0) must hash the same. Combining a
// single shared type tag with the value converted to
// number_float_t keeps the hash consistent with operator== for
// every pair of numbers it considers equal.
// operator== compares numbers by their mathematical value across
// number_integer, number_unsigned, and number_float, so equal
// numbers of different internal types (0, 0U, 0.0) must hash the
// same. Two equal numbers have the same value, which converts to
// the same number_float_t, so all numbers share one type tag and
// hash that converted value. Adding zero turns -0.0 (equal to 0)
// into 0.0, as std::hash need not map both to the same hash.
const auto number_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
const auto h = std::hash<number_float_t> {}(j.template get<number_float_t>());
const auto value = j.template get<number_float_t>() + static_cast<number_float_t>(0);
const auto h = std::hash<number_float_t> {}(value);
return combine(number_type, h);
}
+9 -7
View File
@@ -7717,14 +7717,16 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
case BasicJsonType::value_t::number_unsigned:
case BasicJsonType::value_t::number_float:
{
// operator== converts between number_integer, number_unsigned, and
// number_float before comparing, so equal numbers of different
// internal types (0, 0U, 0.0) must hash the same. Combining a
// single shared type tag with the value converted to
// number_float_t keeps the hash consistent with operator== for
// every pair of numbers it considers equal.
// operator== compares numbers by their mathematical value across
// number_integer, number_unsigned, and number_float, so equal
// numbers of different internal types (0, 0U, 0.0) must hash the
// same. Two equal numbers have the same value, which converts to
// the same number_float_t, so all numbers share one type tag and
// hash that converted value. Adding zero turns -0.0 (equal to 0)
// into 0.0, as std::hash need not map both to the same hash.
const auto number_type = static_cast<std::size_t>(BasicJsonType::value_t::number_float);
const auto h = std::hash<number_float_t> {}(j.template get<number_float_t>());
const auto value = j.template get<number_float_t>() + static_cast<number_float_t>(0);
const auto h = std::hash<number_float_t> {}(value);
return combine(number_type, h);
}
+17
View File
@@ -12,6 +12,7 @@
using json = nlohmann::json;
using ordered_json = nlohmann::ordered_json;
#include <limits>
#include <set>
#include <string>
#include <unordered_set>
@@ -147,6 +148,22 @@ TEST_CASE("hash<nlohmann::json>")
// a std::unordered_set relies on this same consistency between == and hash
const std::unordered_set<json> numbers {json(0), json(static_cast<unsigned>(0)), json(0.0)};
CHECK(numbers.size() == 1);
// -0.0 compares equal to 0 and 0.0
CHECK(json(-0.0) == json(0));
CHECK(std::hash<json> {}(json(-0.0)) == std::hash<json> {}(json(0)));
CHECK(std::hash<json> {}(json(-0.0)) == std::hash<json> {}(json(0.0)));
// the ends of the integer ranges, which equal floats exactly
const auto int_min = (std::numeric_limits<json::number_integer_t>::min)();
const auto int_max = (std::numeric_limits<json::number_integer_t>::max)();
const auto two_63 = json::number_unsigned_t(1) << 63U;
CHECK(json(int_min) == json(-9223372036854775808.0));
CHECK(std::hash<json> {}(json(int_min)) == std::hash<json> {}(json(-9223372036854775808.0)));
CHECK(json(two_63) == json(9223372036854775808.0));
CHECK(std::hash<json> {}(json(two_63)) == std::hash<json> {}(json(9223372036854775808.0)));
CHECK(json(json::number_unsigned_t(int_max)) == json(int_max));
CHECK(std::hash<json> {}(json(json::number_unsigned_t(int_max))) == std::hash<json> {}(json(int_max)));
}
TEST_CASE("hash<nlohmann::ordered_json>")