// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #pragma once #include "Engine/Core/Memory/Memory.h" #include "Engine/Core/Memory/Allocation.h" #include "Engine/Core/Collections/HashFunctions.h" #include "Engine/Core/Collections/Config.h" /// /// Template for unordered set of values (without duplicates with O(1) lookup access). /// /// The type of elements in the set. /// The type of memory allocator. template API_CLASS(InBuild) class HashSet { friend HashSet; public: /// /// Describes single portion of space for the item in a hash map. /// struct Bucket { friend HashSet; enum State : byte { Empty, Deleted, Occupied, }; /// The item. T Item; private: State _state; void Free() { if (_state == Occupied) Memory::DestructItem(&Item); _state = Empty; } void Delete() { _state = Deleted; Memory::DestructItem(&Item); } template void Occupy(const ItemType& item) { Memory::ConstructItems(&Item, &item, 1); _state = Occupied; } FORCE_INLINE bool IsEmpty() const { return _state == Empty; } FORCE_INLINE bool IsDeleted() const { return _state == Deleted; } FORCE_INLINE bool IsOccupied() const { return _state == Occupied; } FORCE_INLINE bool IsNotOccupied() const { return _state != Occupied; } }; typedef typename AllocationType::template Data AllocationData; private: int32 _elementsCount = 0; int32 _size = 0; AllocationData _allocation; public: /// /// Initializes a new instance of the class. /// HashSet() { } /// /// Initializes a new instance of the class. /// /// The initial capacity. HashSet(int32 capacity) { SetCapacity(capacity); } /// /// Initializes a new instance of the class. /// /// The other collection to move. HashSet(HashSet&& other) noexcept : _elementsCount(other._elementsCount) , _size(other._size) { _elementsCount = other._elementsCount; _size = other._size; other._elementsCount = 0; other._size = 0; _allocation.Swap(other._allocation); } /// /// Initializes a new instance of the class. /// /// Other collection to copy HashSet(const HashSet& other) { Clone(other); } /// /// Clones the data from the other collection. /// /// The other collection to copy. /// The reference to this. HashSet& operator=(const HashSet& other) { if (this != &other) Clone(other); return *this; } /// /// Moves the data from the other collection. /// /// The other collection to move. /// The reference to this. HashSet& operator=(HashSet&& other) noexcept { if (this != &other) { Clear(); _allocation.Free(); _elementsCount = other._elementsCount; _size = other._size; other._elementsCount = 0; other._size = 0; _allocation.Swap(other._allocation); } return *this; } /// /// Finalizes an instance of the class. /// ~HashSet() { SetCapacity(0, false); } public: /// /// Gets the amount of the elements in the collection. /// FORCE_INLINE int32 Count() const { return _elementsCount; } /// /// Gets the amount of the elements that can be contained by the collection. /// FORCE_INLINE int32 Capacity() const { return _size; } /// /// Returns true if collection is empty. /// FORCE_INLINE bool IsEmpty() const { return _elementsCount == 0; } /// /// Returns true if collection has one or more elements. /// FORCE_INLINE bool HasItems() const { return _elementsCount != 0; } public: /// /// The hash set collection iterator. /// struct Iterator { friend HashSet; private: HashSet* _collection; int32 _index; Iterator(HashSet* collection, const int32 index) : _collection(collection) , _index(index) { } Iterator(HashSet const* collection, const int32 index) : _collection(const_cast(collection)) , _index(index) { } public: Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) { } Iterator(Iterator&& i) : _collection(i._collection) , _index(i._index) { } public: FORCE_INLINE bool IsEnd() const { return _index == _collection->_size; } FORCE_INLINE bool IsNotEnd() const { return _index != _collection->_size; } FORCE_INLINE Bucket& operator*() const { return _collection->_allocation.Get()[_index]; } FORCE_INLINE Bucket* operator->() const { return &_collection->_allocation.Get()[_index]; } FORCE_INLINE explicit operator bool() const { return _index >= 0 && _index < _collection->_size; } FORCE_INLINE bool operator !() const { return !(bool)*this; } FORCE_INLINE bool operator==(const Iterator& v) const { return _index == v._index && _collection == v._collection; } FORCE_INLINE bool operator!=(const Iterator& v) const { return _index != v._index || _collection != v._collection; } Iterator& operator=(const Iterator& v) { _collection = v._collection; _index = v._index; return *this; } Iterator& operator++() { const int32 capacity = _collection->_size; if (_index != capacity) { const Bucket* data = _collection->_allocation.Get(); do { _index++; } while (_index != capacity && data[_index].IsNotOccupied()); } return *this; } Iterator operator++(int) { Iterator i = *this; ++i; return i; } Iterator& operator--() { if (_index > 0) { const Bucket* data = _collection->_allocation.Get(); do { _index--; } while (_index > 0 && data[_index].IsNotOccupied()); } return *this; } Iterator operator--(int) { Iterator i = *this; --i; return i; } }; public: /// /// Removes all elements from the collection. /// void Clear() { if (_elementsCount != 0) { Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) data[i].Free(); _elementsCount = 0; } } /// /// Clears the collection and delete value objects. /// Note: collection must contain pointers to the objects that have public destructor and be allocated using New method. /// #if defined(_MSC_VER) template::Value>::Type> #endif void ClearDelete() { for (Iterator i = Begin(); i.IsNotEnd(); ++i) { if (i->Item) ::Delete(i->Item); } Clear(); } /// /// Changes capacity of the collection /// /// New capacity /// Enable/disable preserving collection contents during resizing void SetCapacity(int32 capacity, bool preserveContents = true) { if (capacity == Capacity()) return; ASSERT(capacity >= 0); AllocationData oldAllocation; oldAllocation.Swap(_allocation); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _elementsCount = 0; if (capacity != 0 && (capacity & (capacity - 1)) != 0) { // Align capacity value to the next power of two (http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2) capacity--; capacity |= capacity >> 1; capacity |= capacity >> 2; capacity |= capacity >> 4; capacity |= capacity >> 8; capacity |= capacity >> 16; capacity++; } if (capacity) { _allocation.Allocate(capacity); Bucket* data = _allocation.Get(); for (int32 i = 0; i < capacity; i++) data[i]._state = Bucket::Empty; } _size = capacity; Bucket* oldData = oldAllocation.Get(); if (oldElementsCount != 0 && preserveContents) { // TODO; move keys and values on realloc for (int32 i = 0; i < oldSize; i++) { if (oldData[i].IsOccupied()) Add(oldData[i].Item); } } if (oldElementsCount != 0) { for (int32 i = 0; i < oldSize; i++) oldData[i].Free(); } } /// /// Ensures that collection has given capacity. /// /// The minimum required capacity. /// True if preserve collection data when changing its size, otherwise collection after resize will be empty. void EnsureCapacity(int32 minCapacity, bool preserveContents = true) { if (Capacity() >= minCapacity) return; if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) minCapacity = DICTIONARY_DEFAULT_CAPACITY; const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); SetCapacity(capacity, preserveContents); } public: /// /// Add element to the collection. /// /// The element to add to the set. /// True if element has been added to the collection, otherwise false if the element is already present. template bool Add(const ItemType& item) { // Ensure to have enough memory for the next item (in case of new element insertion) EnsureCapacity(_elementsCount + 1); // Find location of the item or place to insert it FindPositionResult pos; FindPosition(item, pos); // Check if object has been already added if (pos.ObjectIndex != -1) return false; // Insert ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; bucket->Occupy(item); _elementsCount++; return true; } /// /// Add element at iterator to the collection /// /// Iterator with item to add void Add(const Iterator& i) { ASSERT(i._collection != this && i); const Bucket& bucket = *i; Add(bucket.Item); } /// /// Removes the specified element from the collection. /// /// The element to remove. /// True if cannot remove item from the collection because cannot find it, otherwise false. template bool Remove(const ItemType& item) { if (IsEmpty()) return false; FindPositionResult pos; FindPosition(item, pos); if (pos.ObjectIndex != -1) { _allocation.Get()[pos.ObjectIndex].Delete(); _elementsCount--; return true; } return false; } /// /// Removes an element at specified iterator position. /// /// The element iterator to remove. /// True if cannot remove item from the collection because cannot find it, otherwise false. bool Remove(const Iterator& i) { ASSERT(i._collection == this); if (i) { ASSERT(_allocation.Get()[i._index].IsOccupied()); _allocation.Get()[i._index].Delete(); _elementsCount--; return true; } return false; } public: /// /// Find element with given item in the collection /// /// Item to find /// Iterator for the found element or End if cannot find it template Iterator Find(const ItemType& item) const { if (IsEmpty()) return End(); FindPositionResult pos; FindPosition(item, pos); return pos.ObjectIndex != -1 ? Iterator(this, pos.ObjectIndex) : End(); } /// /// Determines whether a collection contains the specified element. /// /// The item to locate. /// True if value has been found in a collection, otherwise false template bool Contains(const ItemType& item) const { if (IsEmpty()) return false; FindPositionResult pos; FindPosition(item, pos); return pos.ObjectIndex != -1; } public: /// /// Clones other collection into this /// /// Other collection to clone void Clone(const HashSet& other) { Clear(); SetCapacity(other.Capacity(), false); for (Iterator i = other.Begin(); i != other.End(); ++i) Add(i); ASSERT(Count() == other.Count()); ASSERT(Capacity() == other.Capacity()); } public: Iterator Begin() const { Iterator i(this, -1); ++i; return i; } Iterator End() const { return Iterator(this, _size); } Iterator begin() { Iterator i(this, -1); ++i; return i; } FORCE_INLINE Iterator end() { return Iterator(this, _size); } const Iterator begin() const { Iterator i(this, -1); ++i; return i; } FORCE_INLINE const Iterator end() const { return Iterator(this, _size); } protected: /// /// The result container of the set item lookup searching. /// struct FindPositionResult { int32 ObjectIndex; int32 FreeSlotIndex; }; /// /// Returns a pair of positions: 1st where the object is, 2nd where /// it would go if you wanted to insert it. 1st is -1 /// if object is not found; 2nd is -1 if it is. /// Note: because of deletions where-to-insert is not trivial: it's the /// first deleted bucket we see, as long as we don't find the item later /// /// The item to find /// Pair of values: where the object is and where it would go if you wanted to insert it template void FindPosition(const ItemType& item, FindPositionResult& result) const { ASSERT(_size); const int32 tableSizeMinusOne = _size - 1; int32 bucketIndex = GetHash(item) & tableSizeMinusOne; int32 insertPos = -1; int32 numChecks = 0; const Bucket* data = _allocation.Get(); result.FreeSlotIndex = -1; while (numChecks < _size) { // Empty bucket const Bucket& bucket = data[bucketIndex]; if (bucket.IsEmpty()) { // Found place to insert result.ObjectIndex = -1; result.FreeSlotIndex = insertPos == -1 ? bucketIndex : insertPos; return; } // Deleted bucket if (bucket.IsDeleted()) { // Keep searching but mark to insert if (insertPos == -1) insertPos = bucketIndex; } // Occupied bucket by target item else if (bucket.Item == item) { // Found item result.ObjectIndex = bucketIndex; return; } numChecks++; bucketIndex = (bucketIndex + DICTIONARY_PROB_FUNC(_size, numChecks)) & tableSizeMinusOne; } result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } };