mirror of
https://github.com/nlohmann/json.git
synced 2026-10-08 15:37:13 +00:00
Make std::hash<basic_json> consistent with operator== for numbers (#5772)
* Make std::hash<basic_json> consistent with operator== for numbers operator== converts between number_integer, number_unsigned, and number_float before comparing, so json(0), json(0U), and json(0.0) all compare equal. hash() folded the specific value_t into the result for each of the three numeric cases, giving each a distinct hash and breaking the standard Hash requirement that a == b implies hash(a) == hash(b). A std::unordered_set could therefore hold all three as separate elements even though they compare equal. hash() now treats all three numeric variants the same way: it converts the value to number_float_t and combines it with a single shared type tag, so any two numbers operator== considers equal hash identically regardless of which internal type actually holds them. Updated the accompanying test to check this consistency directly (including via an actual unordered_set) instead of asserting that 0, 0U, and 0.0 hash differently, since that assumption was the bug. Also corrected the function's own doc comment and the std::hash API docs, which described the old behavior as intended. Fixes #5400 Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove now-unused number_integer_t/number_unsigned_t typedefs in hash() Merging the three numeric branches into one that only reads number_float_t left these two aliases unused, which several CI configurations treat as a build error under -Wunused-local-typedefs. Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Mark the unordered_set in the hash regression test const clang-tidy's misc-const-correctness check flagged it: the set is never mutated after construction, only read via size(). Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> * 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> * 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 <mail@nlohmann.me> --------- Signed-off-by: Afonso Januário <afonso-januario@hotmail.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-authored-by: Afonso Januário <afonso-januario@hotmail.com>
This commit is contained in:
1 parent
43fe8928e1
commit
a5f5d3059b
6 files changed
+95
-47
No files matched your search
@@ -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 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.
|
||||
|
||||
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
|
||||
@@ -55,8 +57,6 @@ template<typename BasicJsonType>
|
||||
std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
|
||||
{
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using number_integer_t = typename BasicJsonType::number_integer_t;
|
||||
using number_unsigned_t = typename BasicJsonType::number_unsigned_t;
|
||||
using number_float_t = typename BasicJsonType::number_float_t;
|
||||
|
||||
const auto type = static_cast<std::size_t>(j.type());
|
||||
@@ -113,21 +113,24 @@ std::size_t hash(const BasicJsonType& j, const std::size_t depth = 0)
|
||||
}
|
||||
|
||||
case BasicJsonType::value_t::number_integer:
|
||||
{
|
||||
const auto h = std::hash<number_integer_t> {}(j.template get<number_integer_t>());
|
||||
return combine(type, h);
|
||||
}
|
||||
|
||||
case BasicJsonType::value_t::number_unsigned:
|
||||
{
|
||||
const auto h = std::hash<number_unsigned_t> {}(j.template get<number_unsigned_t>());
|
||||
return combine(type, h);
|
||||
}
|
||||
|
||||
case BasicJsonType::value_t::number_float:
|
||||
{
|
||||
const auto h = std::hash<number_float_t> {}(j.template get<number_float_t>());
|
||||
return combine(type, h);
|
||||
// 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.
|
||||
// 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<std::size_t>(BasicJsonType::value_t::number_float);
|
||||
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);
|
||||
}
|
||||
|
||||
case BasicJsonType::value_t::binary:
|
||||
|
||||
Reference in new issue
Block a user