// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. #pragma once #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/HashFunctions.h" #include "Engine/Core/Collections/Config.h" /// /// Template for unordered dictionary with mapped key with value pairs. /// template API_CLASS(InBuild) class Dictionary { friend Dictionary; public: /// /// Describes single portion of space for the key and value pair in a hash map. /// struct Bucket { friend Dictionary; public: enum State : byte { Empty, Deleted, Occupied, }; public: /// The key. KeyType Key; /// The value. ValueType Value; private: State _state; public: Bucket() : _state(Empty) { } ~Bucket() { } public: void Free() { _state = Empty; } void Delete() { _state = Deleted; } template void Occupy(const KeyComparableType& key) { Key = key; _state = Occupied; } template void Occupy(const KeyComparableType& key, const ValueType& value) { Key = key; Value = value; _state = Occupied; } template void Occupy(const KeyComparableType& key, ValueType&& value) { Key = key; Value = MoveTemp(value); _state = Occupied; } public: FORCE_INLINE bool IsEmpty() const { return _state == Empty; } FORCE_INLINE bool IsNotEmpty() const { return _state != Empty; } FORCE_INLINE bool IsDeleted() const { return _state == Deleted; } FORCE_INLINE bool IsNotDeleted() const { return _state != Deleted; } FORCE_INLINE bool IsOccupied() const { return _state == Occupied; } FORCE_INLINE bool IsNotOccupied() const { return _state != Occupied; } }; private: int32 _elementsCount = 0; int32 _deletedCount = 0; int32 _tableSize = 0; Bucket* _table = nullptr; public: /// /// Initializes a new instance of the class. /// Dictionary() { } /// /// Initializes a new instance of the class. /// /// The initial capacity. Dictionary(int32 capacity) { SetCapacity(capacity); } /// /// Initializes a new instance of the class. /// /// Other collection to copy Dictionary(const Dictionary& other) { Clone(other); } /// /// Clones the data from the other collection. /// /// The other collection to copy. /// The reference to this. Dictionary& operator=(const Dictionary& other) { // Ensure we're not trying to set to itself if (this != &other) Clone(other); return *this; } /// /// Finalizes an instance of the class. /// ~Dictionary() { Cleanup(); } public: /// /// Gets the amount of the elements in the collection. /// /// The amount of elements in the collection. FORCE_INLINE int32 Count() const { return _elementsCount; } /// /// Gets the amount of the elements that can be contained by the collection. /// /// The capacity of the collection. FORCE_INLINE int32 Capacity() const { return _tableSize; } /// /// Returns true if collection is empty. /// /// True if is empty, otherwise false. FORCE_INLINE bool IsEmpty() const { return _elementsCount == 0; } /// /// Returns true if collection has one or more elements. /// /// True if isn't empty, otherwise false. FORCE_INLINE bool HasItems() const { return _elementsCount != 0; } public: /// /// The dictionary collection iterator. /// struct Iterator { friend Dictionary; private: Dictionary& _collection; int32 _index; Iterator(Dictionary& collection, const int32 index) : _collection(collection) , _index(index) { } Iterator(Dictionary const& collection, const int32 index) : _collection((Dictionary&)collection) , _index(index) { } public: Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) { } public: /// /// Checks if iterator is in the end of the collection. /// /// True if is in the end, otherwise false. FORCE_INLINE bool IsEnd() const { return _index == _collection._tableSize; } /// /// Checks if iterator is not in the end of the collection. /// /// True if is not in the end, otherwise false. FORCE_INLINE bool IsNotEnd() const { return _index != _collection._tableSize; } public: FORCE_INLINE Bucket& operator*() const { return _collection._table[_index]; } FORCE_INLINE Bucket* operator->() const { return &_collection._table[_index]; } FORCE_INLINE explicit operator bool() const { return _index >= 0 && _index < _collection._tableSize; } 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; } public: Iterator& operator++() { const int32 capacity = _collection.Capacity(); if (_index != capacity) { do { _index++; } while (_index != capacity && _collection._table[_index].IsNotOccupied()); } return *this; } Iterator operator++(int) { Iterator i = *this; ++i; return i; } Iterator& operator--() { if (_index > 0) { do { _index--; } while (_index > 0 && _collection._table[_index].IsNotOccupied()); } return *this; } Iterator operator--(int) { Iterator i = *this; --i; return i; } }; public: /// /// Gets element by the key (will add default ValueType element if key not found). /// /// The key of the element. /// The value that is at given index. template ValueType& At(const KeyComparableType& key) { // Ensure to have enough memory for the next item (in case of new element insertion) EnsureCapacity(_elementsCount + _deletedCount + 1); // Find location of the item or place to insert it FindPositionResult pos; FindPosition(key, pos); // Check if that key has been already added if (pos.ObjectIndex != -1) { return _table[pos.ObjectIndex].Value; } // Insert ASSERT(pos.FreeSlotIndex != -1); auto bucket = &_table[pos.FreeSlotIndex]; bucket->Occupy(key); _elementsCount++; return bucket->Value; } /// /// Gets the element by the key. /// /// The ky of the element. /// The value that is at given index. template const ValueType& At(const KeyComparableType& key) const { FindPositionResult pos; FindPosition(key, pos); ASSERT(pos.ObjectIndex != -1); return _table[pos.ObjectIndex].Value; } /// /// Gets or sets the element by the key. /// /// The key of the element. /// The value that is at given index. template FORCE_INLINE ValueType& operator[](const KeyComparableType& key) { return At(key); } /// /// Gets or sets the element by the key. /// /// The ky of the element. /// The value that is at given index. template FORCE_INLINE const ValueType& operator[](const KeyComparableType& key) const { return At(key); } /// /// Tries to get element with given key. /// /// The key of the element. /// The result value. /// True if element of given key has been found, otherwise false. template bool TryGet(const KeyComparableType& key, ValueType& result) const { if (IsEmpty()) return false; FindPositionResult pos; FindPosition(key, pos); if (pos.ObjectIndex == -1) return false; result = _table[pos.ObjectIndex].Value; return true; } /// /// Tries to get pointer to the element with given key. /// /// The ky of the element. /// Pointer to the element value or null if cannot find it. template ValueType* TryGet(const KeyComparableType& key) const { if (IsEmpty()) return nullptr; FindPositionResult pos; FindPosition(key, pos); if (pos.ObjectIndex == -1) return nullptr; return &_table[pos.ObjectIndex].Value; } public: /// /// Clears the collection but without changing its capacity (all inserted elements: keys and values will be removed). /// void Clear() { if (_table) { // Free all buckets // Note: this will not clear allocated objects space! for (int32 i = 0; i < _tableSize; i++) { _table[i].Free(); } _elementsCount = _deletedCount = 0; } } /// /// Clears the collection and delete value objects. /// void ClearDelete() { for (auto i = Begin(); i.IsNotEnd(); ++i) { if (i->Value) ::Delete(i->Value); } Clear(); } /// /// Changes the capacity of the collection. /// /// The new capacity. /// Enables preserving collection contents during resizing. void SetCapacity(int32 capacity, bool preserveContents = true) { // Validate input ASSERT(capacity >= 0); // Check if capacity won't change if (capacity == Capacity()) return; // Cache previous state Bucket* oldTable = _table; int32 oldTableSize = _tableSize; // Clear elements counters const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; // Check if need to create a new table if (capacity > 0) { // Align capacity value to the next power of two (if it's not) if ((capacity & (capacity - 1)) != 0) { capacity++; capacity |= capacity >> 1; capacity |= capacity >> 2; capacity |= capacity >> 4; capacity |= capacity >> 8; capacity |= capacity >> 16; capacity = capacity + 1; } // Allocate new table _table = NewArray(capacity); _tableSize = capacity; // Check if preserve content if (oldElementsCount != 0 && preserveContents) { // Try to preserve all pairs in the collection for (int32 i = 0; i < oldTableSize; i++) { if (oldTable[i].IsOccupied()) Add(oldTable[i].Key, oldTable[i].Value); } } } else { // Clear data _table = nullptr; _tableSize = 0; } ASSERT(preserveContents == false || _elementsCount == oldElementsCount); // Delete old table if (oldTable) { DeleteArray(oldTable, oldTableSize); } } /// /// Increases collection capacity by given extra size (content will be preserved). /// /// The extra size to enlarge collection. FORCE_INLINE void IncreaseCapacity(int32 extraSize) { ASSERT(extraSize >= 0); SetCapacity(Capacity() + extraSize); } /// /// Ensures that collection has given capacity. /// /// The minimum required capacity. void EnsureCapacity(int32 minCapacity) { if (Capacity() >= minCapacity) return; // TODO: improve this, better collection growing and shrinking on remove int32 num = Capacity() == 0 ? DICTIONARY_DEFAULT_CAPACITY : Capacity() * 2; if (num < minCapacity) num = minCapacity; SetCapacity(num); } /// /// Cleanup collection data (changes size to 0 without data preserving). /// FORCE_INLINE void Cleanup() { SetCapacity(0, false); } public: /// /// Add pair element to the collection. /// /// The key. /// The value. /// Weak reference to the stored bucket. template Bucket* Add(const KeyComparableType& key, const ValueType& value) { // Ensure to have enough memory for the next item (in case of new element insertion) EnsureCapacity(_elementsCount + _deletedCount + 1); // Find location of the item or place to insert it FindPositionResult pos; FindPosition(key, pos); // Ensure key is unknown ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); // Insert ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_table[pos.FreeSlotIndex]; bucket->Occupy(key, value); _elementsCount++; return bucket; } /// /// Add pair element to the collection. /// /// The key. /// The value. /// Weak reference to the stored bucket. template Bucket* Add(const KeyComparableType& key, ValueType&& value) { // Ensure to have enough memory for the next item (in case of new element insertion) EnsureCapacity(_elementsCount + _deletedCount + 1); // Find location of the item or place to insert it FindPositionResult pos; FindPosition(key, pos); // Ensure key is unknown ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); // Insert ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_table[pos.FreeSlotIndex]; bucket->Occupy(key, MoveTemp(value)); _elementsCount++; return bucket; } /// /// Add pair element to the collection. /// /// Iterator with key and value. void Add(const Iterator& i) { ASSERT(&i._collection != this && i); Bucket& bucket = *i; Add(bucket.Key, bucket.Value); } /// /// Removes element with a specified key. /// /// The element key to remove. /// True if cannot remove item from the collection because cannot find it, otherwise false. template bool Remove(const KeyComparableType& key) { if (IsEmpty()) return true; FindPositionResult pos; FindPosition(key, pos); if (pos.ObjectIndex != -1) { _table[pos.ObjectIndex].Delete(); _elementsCount--; _deletedCount++; return true; } return false; } /// /// Removes element at specified iterator. /// /// The element iterator to remove. /// True if cannot remove item from the collection because cannot find it, otherwise false. bool Remove(Iterator& i) { ASSERT(&i._collection == this); if (i) { ASSERT(_table[i._index].IsOccupied()); _table[i._index].Delete(); _elementsCount--; _deletedCount++; return true; } return false; } /// /// Removes elements with a specified value /// /// Element value to remove /// The amount of removed items. Zero if nothing changed. int32 RemoveValue(const ValueType& value) { int32 result = 0; for (auto i = Begin(); i.IsNotEnd(); ++i) { if (i->Value == value) { Remove(i); result++; } } return result; } public: /// /// Finds the element with given key in the collection. /// /// The key to find. /// The iterator for the found element or End if cannot find it. template Iterator Find(const KeyComparableType& key) const { if (HasItems()) { for (int32 i = 0; i < _tableSize; i++) { if (_table[i].IsOccupied() && _table[i].Key == key) { return Iterator(*this, i); } } } return End(); } /// /// Checks if given key is in a collection. /// /// The key to find. /// True if key has been found in a collection, otherwise false. template bool ContainsKey(const KeyComparableType& key) const { if (IsEmpty()) return false; FindPositionResult pos; FindPosition(key, pos); return pos.ObjectIndex != -1; } /// /// Checks if given value is in a collection. /// /// The value to find. /// True if value has been found in a collection, otherwise false. bool ContainsValue(const ValueType& value) const { if (HasItems()) { for (int32 i = 0; i < _tableSize; i++) { if (_table[i].IsOccupied() && _table[i].Value == value) return true; } } return false; } /// /// Searches for the specified object and returns the zero-based index of the first occurrence within the entire dictionary. /// /// The value of the key to find. /// The output key. /// True if value has been found, otherwise false. bool KeyOf(const ValueType& value, KeyType* key) const { if (HasItems()) { for (int32 i = 0; i < _tableSize; i++) { if (_table[i].IsOccupied() && _table[i].Value == value) { if (key) *key = _table[i].Key; return true; } } } return false; } public: /// /// Clones other collection into this. /// /// The other collection to clone. void Clone(const Dictionary& other) { Clear(); SetCapacity(other.Capacity(), false); for (auto i = other.Begin(); i != other.End(); ++i) Add(i); ASSERT(Count() == other.Count()); ASSERT(Capacity() == other.Capacity()); } /// /// Gets the keys collection to the output array (will contain unique items). /// /// The result. void GetKeys(Array& result) const { for (auto i = Begin(); i.IsNotEnd(); ++i) result.Add(i->Key); } /// /// Gets the values collection to the output array (may contain duplicates). /// /// The result. void GetValues(Array& result) const { for (auto i = Begin(); i.IsNotEnd(); ++i) result.Add(i->Value); } public: /// /// Gets iterator for beginning of the collection. /// /// Iterator for beginning of the collection. Iterator Begin() const { Iterator i(*this, -1); ++i; return i; } /// /// Gets iterator for ending of the collection. /// /// Iterator for ending of the collection. Iterator End() const { return Iterator(*this, _tableSize); } Iterator begin() { Iterator i(*this, -1); ++i; return i; } FORCE_INLINE Iterator end() { return Iterator(*this, _tableSize); } const Iterator begin() const { Iterator i(*this, -1); ++i; return i; } FORCE_INLINE const Iterator end() const { return Iterator(*this, _tableSize); } protected: /// /// The result container of the dictionary 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 key later /// /// The ky to find. /// The pair of values: where the object is and where it would go if you wanted to insert it. template void FindPosition(const KeyComparableType& key, FindPositionResult& result) const { ASSERT(_table); const int32 tableSizeMinusOne = _tableSize - 1; int32 bucketIndex = GetHash(key) & tableSizeMinusOne; int32 insertPos = -1; int32 numChecks = 0; result.FreeSlotIndex = -1; while (numChecks < _tableSize) { // Empty bucket if (_table[bucketIndex].IsEmpty()) { // Found place to insert result.ObjectIndex = -1; result.FreeSlotIndex = insertPos == -1 ? bucketIndex : insertPos; return; } // Deleted bucket if (_table[bucketIndex].IsDeleted()) { // Keep searching but mark to insert if (insertPos == -1) insertPos = bucketIndex; } // Occupied bucket by target key else if (_table[bucketIndex].Key == key) { // Found key result.ObjectIndex = bucketIndex; return; } numChecks++; bucketIndex = (bucketIndex + DICTIONARY_PROB_FUNC(_tableSize, numChecks)) & tableSizeMinusOne; } result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } };