Repository navigation
Make std::hash<basic_json> consistent with operator== for numbers - #5447
afonsojanu wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
@afonsojanu Please fix the DCO requirements. Otherwise good to go.
|
This also fixes #5256, which is a duplicate report of the same There was a separate attempt at this in #5262, but it turned out to have a correctness bug of its own (hashing large integers in their native domain instead of through Fixes #5256 — posted by Claude Code on behalf of @nlohmann |
nlohmann
left a comment
There was a problem hiding this comment.
Please rebase to the latest develop branch to resolve conflicts and sign-off your commits to solve the DCO requirements.
|
@afonsojanu Are you willing to continue working on this? |
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>
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>
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>
46c7a6c to
442ff1c
Compare
|
Yes, definitely willing to keep going on this one. Rebased onto the latest develop and resolved the one real conflict, which was in the hash test file where the recursion-depth-limit work (#5546) added new test helpers in the same spot my unordered_set include landed - kept both, nothing from either side was dropped. The hash.hpp and single_include changes merged cleanly since the numeric-hash case I touched doesn't overlap with the object/array recursion changes upstream made. All three commits now carry a Signed-off-by trailer, and I double checked the DCO check is green on the branch. Ran unit-hash.cpp standalone against the rebased single_include header afterward and all cases pass, including the deep-nesting ones from #5546, so the fix and the newer recursion work coexist fine. Let me know if you'd like anything else adjusted. |
|
Closing this one too, same reason: starting fresh and clearing out everything I had open. |
|
@afonsojanu Thanks a lot for this fix! Since you closed the PR to start fresh, I continued it in #5772: your three commits are included unchanged with your authorship, plus a small follow-up (normalizing — written by Claude Code on behalf of @nlohmann |
Closes #5400.
operator==converts betweennumber_integer,number_unsigned, andnumber_floatbefore comparing, sojson(0),json(0U), andjson(0.0)all compare equal.hash()folded the specificvalue_tinto the result for each of the three numeric cases, giving each a distinct hash and breaking the standardHashrequirement thata == bimplieshash(a) == hash(b). Astd::unordered_setcould hold all three as separate elements even though they compare equal, which is exactly the reproduction in the issue.hash()now treats all three numeric variants the same way: it converts the value tonumber_float_tand combines it with a single shared type tag, so any two numbersoperator==considers equal hash identically regardless of which internal type actually holds them.I also updated the accompanying test to check this consistency directly (including via an actual
unordered_set) instead of asserting that0,0U, and0.0hash differently, since that assumption was the bug. The function's own doc comment and thestd::hashAPI docs described the old behavior as intended, so I corrected those too, and regenerated the doc example's output sincehash(0)andhash(0U)are no longer different values.Tested locally by building the amalgamated header and compiling the full test suite (minus the parts that need the separately-downloaded test data submodule, which isn't available in my environment) plus
unit-hash.cppin isolation against both the fixed and unfixed code, confirming the new assertions fail against the originalhash()with the exact numbers from the issue and pass with the fix.