diff options
author | Evan Brown <ezb@google.com> | 2024-06-06 11:12:29 -0700 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2024-06-06 11:13:25 -0700 |
commit | 66ef711d6846771b8e725e377de37bedae6c1527 (patch) | |
tree | ab97f865d2fb443845dc604e21a0721123b04c3c | |
parent | ed34153e0d9cd15f9b9eb45e86b457e0a495aeea (diff) |
Add validation that hash/eq functors are consistent, meaning that `eq(k1, k2) -> hash(k1) == hash(k2)`.
Also add missing includes/dependencies in flat_hash_map_test.
PiperOrigin-RevId: 640959222
Change-Id: I8d99544af05e97310045e6149f6ef6f7c82e552d
-rw-r--r-- | absl/container/BUILD.bazel | 2 | ||||
-rw-r--r-- | absl/container/CMakeLists.txt | 2 | ||||
-rw-r--r-- | absl/container/flat_hash_map_test.cc | 36 | ||||
-rw-r--r-- | absl/container/internal/raw_hash_set.h | 49 | ||||
-rw-r--r-- | absl/container/internal/raw_hash_set_test.cc | 34 |
5 files changed, 123 insertions, 0 deletions
diff --git a/absl/container/BUILD.bazel b/absl/container/BUILD.bazel index 859163f8..2e32b4dd 100644 --- a/absl/container/BUILD.bazel +++ b/absl/container/BUILD.bazel @@ -265,11 +265,13 @@ cc_test( deps = [ ":flat_hash_map", ":hash_generator_testing", + ":hash_policy_testing", ":test_allocator", ":unordered_map_constructor_test", ":unordered_map_lookup_test", ":unordered_map_members_test", ":unordered_map_modifiers_test", + "//absl/base:config", "//absl/log:check", "//absl/meta:type_traits", "//absl/types:any", diff --git a/absl/container/CMakeLists.txt b/absl/container/CMakeLists.txt index b1f5f9d8..a1023def 100644 --- a/absl/container/CMakeLists.txt +++ b/absl/container/CMakeLists.txt @@ -306,8 +306,10 @@ absl_cc_test( DEPS absl::any absl::check + absl::config absl::flat_hash_map absl::hash_generator_testing + absl::hash_policy_testing absl::test_allocator absl::type_traits absl::unordered_map_constructor_test diff --git a/absl/container/flat_hash_map_test.cc b/absl/container/flat_hash_map_test.cc index 8ef1a62b..eaa86925 100644 --- a/absl/container/flat_hash_map_test.cc +++ b/absl/container/flat_hash_map_test.cc @@ -16,12 +16,16 @@ #include <cstddef> #include <memory> +#include <string> #include <type_traits> #include <utility> #include <vector> +#include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/base/config.h" #include "absl/container/internal/hash_generator_testing.h" +#include "absl/container/internal/hash_policy_testing.h" #include "absl/container/internal/test_allocator.h" #include "absl/container/internal/unordered_map_constructor_test.h" #include "absl/container/internal/unordered_map_lookup_test.h" @@ -363,6 +367,38 @@ TEST(FlatHashMap, FlatHashMapPolicyDestroyReturnsTrue) { std::allocator<char>>(nullptr, nullptr))())); } +struct InconsistentHashEqType { + InconsistentHashEqType(int v1, int v2) : v1(v1), v2(v2) {} + template <typename H> + friend H AbslHashValue(H h, InconsistentHashEqType t) { + return H::combine(std::move(h), t.v1); + } + bool operator==(InconsistentHashEqType t) const { return v2 == t.v2; } + int v1, v2; +}; + +TEST(Iterator, InconsistentHashEqFunctorsValidation) { + if (!IsAssertEnabled()) GTEST_SKIP() << "Assertions not enabled."; + + absl::flat_hash_map<InconsistentHashEqType, int> m; + for (int i = 0; i < 10; ++i) m[{i, i}] = 1; + // We need to insert multiple times to guarantee that we get the assertion + // because it's possible for the hash to collide with the inserted element + // that has v2==0. In those cases, the new element won't be inserted. + auto insert_conflicting_elems = [&] { + for (int i = 100; i < 20000; ++i) { + EXPECT_EQ((m[{i, 0}]), 1); + } + }; + + const char* crash_message = "hash/eq functors are inconsistent."; +#if defined(__arm__) || defined(__aarch64__) + // On ARM, the crash message is garbled so don't expect a specific message. + crash_message = ""; +#endif + EXPECT_DEATH_IF_SUPPORTED(insert_conflicting_elems(), crash_message); +} + } // namespace } // namespace container_internal ABSL_NAMESPACE_END diff --git a/absl/container/internal/raw_hash_set.h b/absl/container/internal/raw_hash_set.h index 2ea07e40..f494fb1c 100644 --- a/absl/container/internal/raw_hash_set.h +++ b/absl/container/internal/raw_hash_set.h @@ -3317,11 +3317,13 @@ class raw_hash_set { template <class K = key_type> iterator find(const key_arg<K>& key, size_t hash) ABSL_ATTRIBUTE_LIFETIME_BOUND { + AssertHashEqConsistent(key); if (is_soo()) return find_soo(key); return find_non_soo(key, hash); } template <class K = key_type> iterator find(const key_arg<K>& key) ABSL_ATTRIBUTE_LIFETIME_BOUND { + AssertHashEqConsistent(key); if (is_soo()) return find_soo(key); prefetch_heap_block(); return find_non_soo(key, hash_ref()(key)); @@ -3822,11 +3824,58 @@ class raw_hash_set { } protected: + // Asserts that hash and equal functors provided by the user are consistent, + // meaning that `eq(k1, k2)` implies `hash(k1)==hash(k2)`. + template <class K> + void AssertHashEqConsistent(ABSL_ATTRIBUTE_UNUSED const K& key) { +#ifndef NDEBUG + if (empty()) return; + + const size_t hash_of_arg = hash_ref()(key); + const auto assert_consistent = [&](const ctrl_t*, slot_type* slot) { + const value_type& element = PolicyTraits::element(slot); + const bool is_key_equal = + PolicyTraits::apply(EqualElement<K>{key, eq_ref()}, element); + if (!is_key_equal) return; + + const size_t hash_of_slot = + PolicyTraits::apply(HashElement{hash_ref()}, element); + const bool is_hash_equal = hash_of_arg == hash_of_slot; + if (!is_hash_equal) { + // In this case, we're going to crash. Do a couple of other checks for + // idempotence issues. Recalculating hash/eq here is also convenient for + // debugging with gdb/lldb. + const size_t once_more_hash_arg = hash_ref()(key); + assert(hash_of_arg == once_more_hash_arg && "hash is not idempotent."); + const size_t once_more_hash_slot = + PolicyTraits::apply(HashElement{hash_ref()}, element); + assert(hash_of_slot == once_more_hash_slot && + "hash is not idempotent."); + const bool once_more_eq = + PolicyTraits::apply(EqualElement<K>{key, eq_ref()}, element); + assert(is_key_equal == once_more_eq && "equality is not idempotent."); + } + assert((!is_key_equal || is_hash_equal) && + "eq(k1, k2) must imply that hash(k1) == hash(k2). " + "hash/eq functors are inconsistent."); + }; + + if (is_soo()) { + assert_consistent(/*unused*/ nullptr, soo_slot()); + return; + } + // We only do validation for small tables so that it's constant time. + if (capacity() > 16) return; + IterateOverFullSlots(common(), slot_array(), assert_consistent); +#endif + } + // Attempts to find `key` in the table; if it isn't found, returns an iterator // where the value can be inserted into, with the control byte already set to // `key`'s H2. Returns a bool indicating whether an insertion can take place. template <class K> std::pair<iterator, bool> find_or_prepare_insert(const K& key) { + AssertHashEqConsistent(key); if (is_soo()) return find_or_prepare_insert_soo(key); return find_or_prepare_insert_non_soo(key); } diff --git a/absl/container/internal/raw_hash_set_test.cc b/absl/container/internal/raw_hash_set_test.cc index 10f793ef..195296a1 100644 --- a/absl/container/internal/raw_hash_set_test.cc +++ b/absl/container/internal/raw_hash_set_test.cc @@ -3293,6 +3293,40 @@ TEST(Table, ReserveToNonSoo) { } } +struct InconsistentHashEqType { + InconsistentHashEqType(int v1, int v2) : v1(v1), v2(v2) {} + template <typename H> + friend H AbslHashValue(H h, InconsistentHashEqType t) { + return H::combine(std::move(h), t.v1); + } + bool operator==(InconsistentHashEqType t) const { return v2 == t.v2; } + int v1, v2; +}; + +TEST(Iterator, InconsistentHashEqFunctorsValidation) { + if (!IsAssertEnabled()) GTEST_SKIP() << "Assertions not enabled."; + + ValueTable<InconsistentHashEqType> t; + for (int i = 0; i < 10; ++i) t.insert({i, i}); + // We need to find/insert multiple times to guarantee that we get the + // assertion because it's possible for the hash to collide with the inserted + // element that has v2==0. In those cases, the new element won't be inserted. + auto find_conflicting_elems = [&] { + for (int i = 100; i < 20000; ++i) { + EXPECT_EQ(t.find({i, 0}), t.end()); + } + }; + EXPECT_DEATH_IF_SUPPORTED(find_conflicting_elems(), + "hash/eq functors are inconsistent."); + auto insert_conflicting_elems = [&] { + for (int i = 100; i < 20000; ++i) { + EXPECT_EQ(t.insert({i, 0}).second, false); + } + }; + EXPECT_DEATH_IF_SUPPORTED(insert_conflicting_elems(), + "hash/eq functors are inconsistent."); +} + REGISTER_TYPED_TEST_SUITE_P( SooTest, Empty, Clear, ClearBug, Contains1, Contains2, ContainsEmpty, CopyConstruct, CopyDifferentCapacities, CopyDifferentSizes, |