diff --git a/docs/mkdocs/docs/api/basic_json/std_hash.md b/docs/mkdocs/docs/api/basic_json/std_hash.md index aaa49ed68..0b42856cb 100644 --- a/docs/mkdocs/docs/api/basic_json/std_hash.md +++ b/docs/mkdocs/docs/api/basic_json/std_hash.md @@ -7,8 +7,9 @@ namespace std { ``` Return a hash value for a JSON object. 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 `#!json null`, `#!cpp 0`, `#!cpp 0U`, and -`#!cpp false`, etc. +type of the JSON value is taken into account, so `#!json null`, `#!cpp false`, and numbers all hash differently from +each other. Numbers that compare equal under [`operator==`](operator_eq.md) always hash equally, regardless of +whether they are stored as `#!cpp 0`, `#!cpp 0U`, or `#!cpp 0.0`. ## Examples diff --git a/docs/mkdocs/docs/examples/std_hash.output b/docs/mkdocs/docs/examples/std_hash.output index 521d2b4b8..f053e6a84 100644 --- a/docs/mkdocs/docs/examples/std_hash.output +++ b/docs/mkdocs/docs/examples/std_hash.output @@ -1,8 +1,8 @@ hash(null) = 2654435769 hash(false) = 2654436030 -hash(0) = 2654436095 -hash(0U) = 2654436156 -hash("") = 6142509191626859748 +hash(0) = 2654436221 +hash(0U) = 2654436221 +hash("") = 11160318156688833227 hash({}) = 2654435832 hash([]) = 2654435899 -hash({"hello": "world"}) = 4469488738203676328 +hash({"hello": "world"}) = 3701319991624763853 diff --git a/include/nlohmann/detail/hash.hpp b/include/nlohmann/detail/hash.hpp index 20a971886..c97e99339 100644 --- a/include/nlohmann/detail/hash.hpp +++ b/include/nlohmann/detail/hash.hpp @@ -35,8 +35,10 @@ std::size_t hash_iteratively(const BasicJsonType& j); @brief hash a JSON value 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 -null, 0, 0U, and false, etc. +type of the JSON value is taken into account, so null, false, and numbers all +hash differently from each other, but any two numbers that compare equal +under operator== hash equally regardless of which of number_integer, +number_unsigned, or number_float actually holds the value. Hashing an array or an object hashes its elements, which used to call this function again once per nesting level, so a value nested deeply enough @@ -113,21 +115,18 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) } case BasicJsonType::value_t::number_integer: - { - const auto h = std::hash {}(j.template get()); - return combine(type, h); - } - case BasicJsonType::value_t::number_unsigned: - { - const auto h = std::hash {}(j.template get()); - return combine(type, h); - } - 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. + const auto number_type = static_cast(BasicJsonType::value_t::number_float); const auto h = std::hash {}(j.template get()); - return combine(type, h); + return combine(number_type, h); } case BasicJsonType::value_t::binary: diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index be32959d5..6a6e0aca9 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7636,8 +7636,10 @@ std::size_t hash_iteratively(const BasicJsonType& j); @brief hash a JSON value 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 -null, 0, 0U, and false, etc. +type of the JSON value is taken into account, so null, false, and numbers all +hash differently from each other, but any two numbers that compare equal +under operator== hash equally regardless of which of number_integer, +number_unsigned, or number_float actually holds the value. Hashing an array or an object hashes its elements, which used to call this function again once per nesting level, so a value nested deeply enough @@ -7714,21 +7716,18 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) } case BasicJsonType::value_t::number_integer: - { - const auto h = std::hash {}(j.template get()); - return combine(type, h); - } - case BasicJsonType::value_t::number_unsigned: - { - const auto h = std::hash {}(j.template get()); - return combine(type, h); - } - 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. + const auto number_type = static_cast(BasicJsonType::value_t::number_float); const auto h = std::hash {}(j.template get()); - return combine(type, h); + return combine(number_type, h); } case BasicJsonType::value_t::binary: diff --git a/tests/src/unit-hash.cpp b/tests/src/unit-hash.cpp index 382dfcaa2..a517291a8 100644 --- a/tests/src/unit-hash.cpp +++ b/tests/src/unit-hash.cpp @@ -14,6 +14,7 @@ using ordered_json = nlohmann::ordered_json; #include #include +#include namespace { @@ -91,6 +92,9 @@ TEST_CASE("hash") // Collect hashes for different JSON values and make sure that they are distinct // We cannot compare against fixed values, because the implementation of // std::hash may differ between compilers. + // + // numbers that compare equal under operator== (0 == 0U == 0.0) must hash + // equally, so they are only inserted once below and checked separately. std::set hashes; @@ -107,10 +111,7 @@ TEST_CASE("hash") // number hashes.insert(std::hash {}(json(0))); - hashes.insert(std::hash {}(json(static_cast(0)))); - hashes.insert(std::hash {}(json(-1))); - hashes.insert(std::hash {}(json(0.0))); hashes.insert(std::hash {}(json(42.23))); // array @@ -132,7 +133,20 @@ TEST_CASE("hash") // discarded hashes.insert(std::hash {}(json(json::value_t::discarded))); - CHECK(hashes.size() == 21); + CHECK(hashes.size() == 19); + + // numbers that compare equal under operator== must hash equally, + // regardless of which of number_integer, number_unsigned, or + // number_float actually holds the value + CHECK(json(0) == json(static_cast(0))); + CHECK(json(0) == json(0.0)); + CHECK(std::hash {}(json(0)) == std::hash {}(json(static_cast(0)))); + CHECK(std::hash {}(json(0)) == std::hash {}(json(0.0))); + CHECK(std::hash {}(json(-1)) == std::hash {}(json(-1.0))); + + // a std::unordered_set relies on this same consistency between == and hash + std::unordered_set numbers {json(0), json(static_cast(0)), json(0.0)}; + CHECK(numbers.size() == 1); } TEST_CASE("hash") @@ -156,10 +170,7 @@ TEST_CASE("hash") // number hashes.insert(std::hash {}(ordered_json(0))); - hashes.insert(std::hash {}(ordered_json(static_cast(0)))); - hashes.insert(std::hash {}(ordered_json(-1))); - hashes.insert(std::hash {}(ordered_json(0.0))); hashes.insert(std::hash {}(ordered_json(42.23))); // array @@ -181,7 +192,10 @@ TEST_CASE("hash") // discarded hashes.insert(std::hash {}(ordered_json(ordered_json::value_t::discarded))); - CHECK(hashes.size() == 21); + CHECK(hashes.size() == 19); + + CHECK(std::hash {}(ordered_json(0)) == std::hash {}(ordered_json(static_cast(0)))); + CHECK(std::hash {}(ordered_json(0)) == std::hash {}(ordered_json(0.0))); } TEST_CASE("hash of deeply nested values")