From f1027d8f3821dec7f86e4a8004fce92c9a899783 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 7 Oct 2026 07:36:29 +0200 Subject: [PATCH] Clarify hash documentation after review - Say "may hash differently" for null, false, and numbers, since a collision across types is possible. - Name the storage types (signed integer, unsigned integer, floating-point number) instead of example literals. - Explain that the hash survives converting an integer to number_float_t but not the lossy conversion back, and that unequal numbers may share a hash. - State that the example hash values are illustrative only and vary by platform, compiler, compiler version, and library version. Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/std_hash.md | 12 +++++++++--- include/nlohmann/detail/hash.hpp | 6 +++++- single_include/nlohmann/json.hpp | 6 +++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/mkdocs/docs/api/basic_json/std_hash.md b/docs/mkdocs/docs/api/basic_json/std_hash.md index cb1243b6d..7be7772cc 100644 --- a/docs/mkdocs/docs/api/basic_json/std_hash.md +++ b/docs/mkdocs/docs/api/basic_json/std_hash.md @@ -7,9 +7,14 @@ 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, so `#!json null`, `#!cpp false`, and numbers all hash differently from +type of the JSON value is taken into account, so `#!json null`, `#!cpp false`, and numbers may 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`. +whether they are stored as signed integer, unsigned integer, or floating-point number. + +Numbers are hashed by their value converted to `number_float_t`. Converting an integer to `number_float_t` therefore +keeps its hash, but converting a floating-point number to an integer type is lossy and can change it: `#!cpp 0.5` +converts to `#!cpp 0`, which need not have the same hash. Unequal numbers can also share a hash value, for example two +large integers that convert to the same `number_float_t`. ## Examples @@ -27,7 +32,8 @@ whether they are stored as `#!cpp 0`, `#!cpp 0U`, or `#!cpp 0.0`. --8<-- "examples/std_hash.output" ``` - Note the output is platform-dependent. + The hash values shown are examples only. They depend on the platform, the compiler, and the compiler version, and + they can change between versions of this library. Do not persist them or rely on specific values. ## See also diff --git a/include/nlohmann/detail/hash.hpp b/include/nlohmann/detail/hash.hpp index aa91f8894..fd1326b5d 100644 --- a/include/nlohmann/detail/hash.hpp +++ b/include/nlohmann/detail/hash.hpp @@ -35,7 +35,7 @@ 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, so null, false, and numbers all +type of the JSON value is taken into account, so null, false, and numbers may 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. @@ -123,6 +123,10 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) // 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. + // The converse does not hold: converting a number_float_t value + // to an integer type is lossy, so the result can hash + // differently, and unequal numbers that convert to the same + // number_float_t (e.g., 2^53 and 2^53 + 1) share a hash. const auto number_type = static_cast(BasicJsonType::value_t::number_float); const auto value = j.template get() + static_cast(0); const auto h = std::hash {}(value); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 95acdfd78..77635787e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7583,7 +7583,7 @@ 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, so null, false, and numbers all +type of the JSON value is taken into account, so null, false, and numbers may 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. @@ -7671,6 +7671,10 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0) // 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. + // The converse does not hold: converting a number_float_t value + // to an integer type is lossy, so the result can hash + // differently, and unequal numbers that convert to the same + // number_float_t (e.g., 2^53 and 2^53 + 1) share a hash. const auto number_type = static_cast(BasicJsonType::value_t::number_float); const auto value = j.template get() + static_cast(0); const auto h = std::hash {}(value);