template <class Elem, class HashSetType> class HashSetIterator { public: using iterator_category = std::forward_iterator_tag; using value_type = Elem; using difference_type = std::ptrdiff_t; using pointer = Elem*; using reference = Elem&;
template <class Elem1, class HashSetType1, class Elem2, class HashSetType2> friendbooloperator==(const HashSetIterator<Elem1, HashSetType1>& lhs, const HashSetIterator<Elem2, HashSetType2>& rhs); template <class T, class EmptyFn, class HashFn, class Pred, class Alloc> friendclass HashSet; template <class OtherElem, class OtherHashSetType> friendclass HashSetIterator;
};
template <class T> using DefaultHashFn = std::conditional_t<std::is_same_v<T, std::string>, DataHash, std::hash<T>>;
struct DefaultStringEquals { // Allow comparison with anything that can be compared to std::string, // for example std::string_view. template <typename T> booloperator()(const std::string& lhs, const T& rhs) const { return lhs == rhs;
}
};
template <class T> using DefaultPred =
std::conditional_t<std::is_same_v<T, std::string>, DefaultStringEquals, std::equal_to<T>>;
// Low memory version of a hash set, uses less memory than std::unordered_multiset since elements // aren't boxed. Uses linear probing to resolve collisions. // EmptyFn needs to implement two functions MakeEmpty(T& item) and IsEmpty(const T& item). // TODO: We could get rid of this requirement by using a bitmap, though maybe this would be slower // and more complicated. template <class T, class EmptyFn = DefaultEmptyFn<T>, class HashFn = DefaultHashFn<T>, class Pred = DefaultPred<T>, class Alloc = std::allocator<T>> class HashSet { public: using value_type = T; using allocator_type = Alloc; using reference = T&; using const_reference = const T&; using pointer = T*; using const_pointer = const T*; using iterator = HashSetIterator<T, HashSet>; using const_iterator = HashSetIterator<const T, const HashSet>; using size_type = size_t; using difference_type = ptrdiff_t;
// If we don't own the data, this will create a new array which owns the data. void clear() {
DeallocateStorage();
num_elements_ = 0;
elements_until_expand_ = 0;
}
// Construct from existing data. // Read from a block of memory, if make_copy_of_data is false, then data_ points to within the // passed in ptr_.
HashSet(const uint8_t* ptr, bool make_copy_of_data, size_t* read_count) noexcept {
uint64_t temp;
size_t offset = 0;
offset = ReadFromBytes(ptr, offset, &temp);
num_elements_ = static_cast<uint64_t>(temp);
offset = ReadFromBytes(ptr, offset, &temp);
num_buckets_ = static_cast<uint64_t>(temp);
CHECK_LE(num_elements_, num_buckets_);
offset = ReadFromBytes(ptr, offset, &temp);
elements_until_expand_ = static_cast<uint64_t>(temp);
offset = ReadFromBytes(ptr, offset, &min_load_factor_);
offset = ReadFromBytes(ptr, offset, &max_load_factor_); if (!make_copy_of_data) {
owns_data_ = false;
data_ = const_cast<T*>(reinterpret_cast<const T*>(ptr + offset));
offset += sizeof(*data_) * num_buckets_;
} else {
AllocateStorage(num_buckets_); // Write elements, not that this may not be safe for cross compilation if the elements are // pointer sized. for (size_t i = 0; i < num_buckets_; ++i) {
offset = ReadFromBytes(ptr, offset, &data_[i]);
}
} // Caller responsible for aligning.
*read_count = offset;
}
// Returns how large the table is after being written. If target is null, then no writing happens // but the size is still returned. Target must be 8 byte aligned.
size_t WriteToMemory(uint8_t* ptr) const {
size_t offset = 0;
offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(num_elements_));
offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(num_buckets_));
offset = WriteToBytes(ptr, offset, static_cast<uint64_t>(elements_until_expand_));
offset = WriteToBytes(ptr, offset, min_load_factor_);
offset = WriteToBytes(ptr, offset, max_load_factor_); // Write elements, not that this may not be safe for cross compilation if the elements are // pointer sized. for (size_t i = 0; i < num_buckets_; ++i) {
offset = WriteToBytes(ptr, offset, data_[i]);
} // Caller responsible for aligning. return offset;
}
HashSet& operator=(const HashSet& other) {
HashSet(other).swap(*this); // NOLINT(runtime/explicit) - a case of lint gone mad. return *this;
}
// Lower case for c++11 for each.
iterator begin() {
iterator ret(this, 0); if (num_buckets_ != 0 && IsFreeSlot(ret.index_)) {
++ret; // Skip all the empty slots.
} return ret;
}
// Lower case for c++11 for each. const version.
const_iterator begin() const {
const_iterator ret(this, 0); if (num_buckets_ != 0 && IsFreeSlot(ret.index_)) {
++ret; // Skip all the empty slots.
} return ret;
}
// Lower case for c++11 for each.
iterator end() { return iterator(this, NumBuckets());
}
// Lower case for c++11 for each. const version.
const_iterator end() const { return const_iterator(this, NumBuckets());
}
size_t size() const { return num_elements_;
}
bool empty() const { return size() == 0;
}
// Erase algorithm: // Make an empty slot where the iterator is pointing. // Scan forwards until we hit another empty slot. // If an element in between doesn't rehash to the range from the current empty slot to the // iterator. It must be before the empty slot, in that case we can move it to the empty slot // and set the empty slot to be the location we just moved from. // Relies on maintaining the invariant that there's no empty slots from the 'ideal' index of an // element to its actual location/index. // Note that since erase shuffles back elements, it may result in the same element being visited // twice during HashSet iteration. This happens when an element already visited during iteration // gets shuffled to the end of the bucket array.
iterator erase(iterator it) { // empty_index is the index that will become empty.
size_t empty_index = it.index_;
DCHECK(!IsFreeSlot(empty_index));
size_t next_index = empty_index; bool filled = false; // True if we filled the empty index. while (true) {
next_index = NextIndex(next_index);
T& next_element = ElementForIndex(next_index); // If the next element is empty, we are done. Make sure to clear the current empty index. if (emptyfn_.IsEmpty(next_element)) {
emptyfn_.MakeEmpty(ElementForIndex(empty_index)); break;
} // Otherwise try to see if the next element can fill the current empty index. const size_t next_hash = hashfn_(next_element); // Calculate the ideal index, if it is within empty_index + 1 to next_index then there is // nothing we can do.
size_t next_ideal_index = IndexForHash(next_hash); // Loop around if needed for our check.
size_t unwrapped_next_index = next_index; if (unwrapped_next_index < empty_index) {
unwrapped_next_index += NumBuckets();
} // Loop around if needed for our check.
size_t unwrapped_next_ideal_index = next_ideal_index; if (unwrapped_next_ideal_index < empty_index) {
unwrapped_next_ideal_index += NumBuckets();
} if (unwrapped_next_ideal_index <= empty_index ||
unwrapped_next_ideal_index > unwrapped_next_index) { // If the target index isn't within our current range it must have been probed from before // the empty index.
ElementForIndex(empty_index) = std::move(next_element);
filled = true; // TODO: Optimize
empty_index = next_index;
}
}
--num_elements_; // If we didn't fill the slot then we need go to the next non free slot. if (!filled) {
++it;
} return it;
}
// Find an element, returns end() if not found. // Allows custom key (K) types, example of when this is useful: // Set of Class* indexed by name, want to find a class with a name but can't allocate // a temporary Class object in the heap for performance solution. template <typename K>
iterator find(const K& key) { return FindWithHash(key, hashfn_(key));
}
// Insert an element with hint. // Note: The hint is not very useful for a HashSet<> unless there are many hash conflicts // and in that case the use of HashSet<> itself should be reconsidered.
std::pair<iterator, bool> insert([[maybe_unused]] const_iterator hint, const T& element) { return insert(element);
}
std::pair<iterator, bool> insert([[maybe_unused]] const_iterator hint, T&& element) { return insert(std::move(element));
}
// Insert an element known not to be in the `HashSet<>`. void Put(const T& element) { return PutWithHash(element, hashfn_(element));
} void Put(T&& element) { return PutWithHash(std::move(element), hashfn_(element));
}
// Reserve enough room to insert until Size() == num_elements without requiring to grow the hash // set. No-op if the hash set is already large enough to do this. void reserve(size_t num_elements) {
size_t num_buckets = num_elements / max_load_factor_; // Deal with rounding errors. Add one for rounding. while (static_cast<size_t>(num_buckets * max_load_factor_) <= num_elements + 1u) {
++num_buckets;
} if (num_buckets > NumBuckets()) {
Resize(num_buckets);
}
}
// To distance that inserted elements were probed. Used for measuring how good hash functions // are.
size_t TotalProbeDistance() const {
size_t total = 0; for (size_t i = 0; i < NumBuckets(); ++i) { const T& element = ElementForIndex(i); if (!emptyfn_.IsEmpty(element)) {
size_t ideal_location = IndexForHash(hashfn_(element)); if (ideal_location > i) {
total += i + NumBuckets() - ideal_location;
} else {
total += i - ideal_location;
}
}
} return total;
}
// Calculate the current load factor and return it. double CalculateLoadFactor() const { returnstatic_cast<double>(size()) / static_cast<double>(NumBuckets());
}
// Make sure that everything reinserts in the right spot. Returns the number of errors.
size_t Verify() NO_THREAD_SAFETY_ANALYSIS {
size_t errors = 0; for (size_t i = 0; i < num_buckets_; ++i) {
T& element = data_[i]; if (!emptyfn_.IsEmpty(element)) {
T temp;
emptyfn_.MakeEmpty(temp);
std::swap(temp, element);
size_t first_slot = FirstAvailableSlot(IndexForHash(hashfn_(temp))); if (i != first_slot) {
LOG(ERROR) << "Element " << i << " should be in slot " << first_slot;
++errors;
}
std::swap(temp, element);
}
} return errors;
}
// Change the load factor of the hash set. If the current load factor is greater than the max // specified, then we resize the hash table storage. void SetLoadFactor(double min_load_factor, double max_load_factor) {
DCHECK_LT(min_load_factor, max_load_factor);
DCHECK_GT(min_load_factor, 0.0);
DCHECK_LT(max_load_factor, 1.0);
min_load_factor_ = min_load_factor;
max_load_factor_ = max_load_factor;
elements_until_expand_ = NumBuckets() * max_load_factor_; // If the current load factor isn't in the range, then resize to the mean of the minimum and // maximum load factor. constdouble load_factor = CalculateLoadFactor(); if (load_factor > max_load_factor_) {
Resize(size() / ((min_load_factor_ + max_load_factor_) * 0.5));
}
}
// The hash set expands when Size() reaches ElementsUntilExpand().
size_t ElementsUntilExpand() const { return elements_until_expand_;
}
// Find the hash table slot for an element, or return NumBuckets() if not found. // This value for not found is important so that iterator(this, FindIndex(...)) == end(). template <typename K>
ALWAYS_INLINE
size_t FindIndex(const K& element, size_t hash) const { // Guard against failing to get an element for a non-existing index. if (UNLIKELY(NumBuckets() == 0)) { return0;
} auto fail_fn = [&]([[maybe_unused]] size_t index) ALWAYS_INLINE { return NumBuckets(); }; return FindIndexImpl(element, hash, fail_fn);
}
// Find the hash table slot for an element, or return an empty slot index if not found. template <bool kCanFind = true, typename K, typename FailFn>
ALWAYS_INLINE
size_t FindIndexImpl(const K& element, size_t hash, FailFn fail_fn) const {
DCHECK_NE(NumBuckets(), 0u);
DCHECK_EQ(hashfn_(element), hash);
size_t index = IndexForHash(hash); while (true) { const T& slot = ElementForIndex(index); if (emptyfn_.IsEmpty(slot)) { return fail_fn(index);
} if (!kCanFind) {
DCHECK(!pred_(slot, element));
} elseif (pred_(slot, element)) { return index;
}
index = NextIndex(index);
}
}
// Allocate a number of buckets. void AllocateStorage(size_t num_buckets) {
num_buckets_ = num_buckets;
data_ = allocfn_.allocate(num_buckets_);
owns_data_ = true; for (size_t i = 0; i < num_buckets_; ++i) {
std::allocator_traits<allocator_type>::construct(allocfn_, std::addressof(data_[i]));
emptyfn_.MakeEmpty(data_[i]);
}
}
void DeallocateStorage() { if (owns_data_) { for (size_t i = 0; i < NumBuckets(); ++i) {
std::allocator_traits<allocator_type>::destroy(allocfn_, std::addressof(data_[i]));
} if (data_ != nullptr) {
allocfn_.deallocate(data_, NumBuckets());
}
owns_data_ = false;
}
data_ = nullptr;
num_buckets_ = 0;
}
// Expand the set based on the load factors. void Expand() {
size_t min_index = static_cast<size_t>(size() / min_load_factor_); // Resize based on the minimum load factor.
Resize(min_index);
}
// Expand / shrink the table to the new specified size. void Resize(size_t new_size) { if (new_size < kMinBuckets) {
new_size = kMinBuckets;
}
DCHECK_GE(new_size, size());
T* const old_data = data_;
size_t old_num_buckets = num_buckets_; // Reinsert all of the old elements. constbool owned_data = owns_data_;
AllocateStorage(new_size); for (size_t i = 0; i < old_num_buckets; ++i) {
T& element = old_data[i]; if (!emptyfn_.IsEmpty(element)) {
data_[FirstAvailableSlot(IndexForHash(hashfn_(element)))] = std::move(element);
} if (owned_data) {
std::allocator_traits<allocator_type>::destroy(allocfn_, std::addressof(element));
}
} if (owned_data) {
allocfn_.deallocate(old_data, old_num_buckets);
}
// When we hit elements_until_expand_, we are at the max load factor and must expand again.
elements_until_expand_ = NumBuckets() * max_load_factor_;
}
ALWAYS_INLINE size_t FirstAvailableSlot(size_t index) const {
DCHECK_LT(index, NumBuckets()); // Don't try to get a slot out of range.
size_t non_empty_count = 0; while (!emptyfn_.IsEmpty(data_[index])) {
index = NextIndex(index);
non_empty_count++;
DCHECK_LE(non_empty_count, NumBuckets()); // Don't loop forever.
} return index;
}
Alloc allocfn_; // Allocator function.
HashFn hashfn_; // Hashing function.
EmptyFn emptyfn_; // IsEmpty/SetEmpty function.
Pred pred_; // Equals function.
size_t num_elements_; // Number of inserted elements.
size_t num_buckets_; // Number of hash table buckets.
size_t elements_until_expand_; // Maximum number of elements until we expand the table. bool owns_data_; // If we own data_ and are responsible for freeing it.
T* data_; // Backing storage. double min_load_factor_; double max_load_factor_;
template <class Elem, class HashSetType> friendclass HashSetIterator;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.