diff --git a/docs/mkdocs/docs/api/basic_json/std_hash.md b/docs/mkdocs/docs/api/basic_json/std_hash.md index 0b42856cb..cb1243b6d 100644 --- a/docs/mkdocs/docs/api/basic_json/std_hash.md +++ b/docs/mkdocs/docs/api/basic_json/std_hash.md @@ -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. diff --git a/docs/mkdocs/docs/examples/std_hash.cpp b/docs/mkdocs/docs/examples/std_hash.cpp index 9721910eb..184ddbbb3 100644 --- a/docs/mkdocs/docs/examples/std_hash.cpp +++ b/docs/mkdocs/docs/examples/std_hash.cpp @@ -11,6 +11,7 @@ int main() << "hash(false) = " << std::hash {}(json(false)) << '\n' << "hash(0) = " << std::hash {}(json(0)) << '\n' << "hash(0U) = " << std::hash {}(json(0U)) << '\n' + << "hash(0.0) = " << std::hash {}(json(0.0)) << '\n' << "hash(\"\") = " << std::hash {}(json("")) << '\n' << "hash({}) = " << std::hash {}(json::object()) << '\n' << "hash([]) = " << std::hash {}(json::array()) << '\n' diff --git a/docs/mkdocs/docs/examples/std_hash.output b/docs/mkdocs/docs/examples/std_hash.output index f053e6a84..ca3207c0a 100644 --- a/docs/mkdocs/docs/examples/std_hash.output +++ b/docs/mkdocs/docs/examples/std_hash.output @@ -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 diff --git a/include/nlohmann/detail/hash.hpp b/include/nlohmann/detail/hash.hpp index cc137fc69..aa91f8894 100644 --- a/include/nlohmann/detail/hash.hpp +++ b/include/nlohmann/detail/hash.hpp @@ -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(BasicJsonType::value_t::number_float); - const auto h = std::hash {}(j.template get()); + const auto value = j.template get() + static_cast(0); + const auto h = std::hash {}(value); return combine(number_type, h); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index b2369a643..2a6fcfa60 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -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(BasicJsonType::value_t::number_float); - const auto h = std::hash {}(j.template get()); + const auto value = j.template get() + static_cast(0); + const auto h = std::hash {}(value); return combine(number_type, h); } diff --git a/tests/src/unit-hash.cpp b/tests/src/unit-hash.cpp index 1d4fde81d..e70b39b5a 100644 --- a/tests/src/unit-hash.cpp +++ b/tests/src/unit-hash.cpp @@ -12,6 +12,7 @@ using json = nlohmann::json; using ordered_json = nlohmann::ordered_json; +#include #include #include #include @@ -147,6 +148,22 @@ TEST_CASE("hash") // a std::unordered_set relies on this same consistency between == and hash const std::unordered_set numbers {json(0), json(static_cast(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(-0.0)) == std::hash {}(json(0))); + CHECK(std::hash {}(json(-0.0)) == std::hash {}(json(0.0))); + + // the ends of the integer ranges, which equal floats exactly + const auto int_min = (std::numeric_limits::min)(); + const auto int_max = (std::numeric_limits::max)(); + const auto two_63 = json::number_unsigned_t(1) << 63U; + CHECK(json(int_min) == json(-9223372036854775808.0)); + CHECK(std::hash {}(json(int_min)) == std::hash {}(json(-9223372036854775808.0))); + CHECK(json(two_63) == json(9223372036854775808.0)); + CHECK(std::hash {}(json(two_63)) == std::hash {}(json(9223372036854775808.0))); + CHECK(json(json::number_unsigned_t(int_max)) == json(int_max)); + CHECK(std::hash {}(json(json::number_unsigned_t(int_max))) == std::hash {}(json(int_max))); } TEST_CASE("hash")