// 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 dictionary with mapped key with value pairs. /// /// The type of the keys in the dictionary. /// The type of the values in the dictionary. /// The type of memory allocator. 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; enum State : byte { Empty = 0, Deleted = 1, Occupied = 2, }; /// The key. KeyType Key; /// The value. ValueType Value; private: State _state; void Free() { if (_state == Occupied) { Memory::DestructItem(&Key); Memory::DestructItem(&Value); } _state = Empty; } void Delete() { _state = Deleted; Memory::DestructItem(&Key); Memory::DestructItem(&Value); } template void Occupy(const KeyComparableType& key) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItem(&Value); _state = Occupied; } template void Occupy(const KeyComparableType& key, const ValueType& value) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItems(&Value, &value, 1); _state = Occupied; } template void Occupy(const KeyComparableType& key, ValueType&& value) { Memory::ConstructItems(&Key, &key, 1); Memory::MoveItems(&Value, &value, 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 _deletedCount = 0; int32 _size = 0; AllocationData _allocation; 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. /// /// The other collection to move. Dictionary(Dictionary&& other) noexcept : _elementsCount(other._elementsCount) , _deletedCount(other._deletedCount) , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } /// /// 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) { if (this != &other) Clone(other); return *this; } /// /// Moves the data from the other collection. /// /// The other collection to move. /// The reference to this. Dictionary& operator=(Dictionary&& other) noexcept { if (this != &other) { Clear(); _allocation.Free(); _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } return *this; } /// /// Finalizes an instance of the class. /// ~Dictionary() { Clear(); } 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 Dictionary collection iterator. /// struct Iterator { friend Dictionary; private: Dictionary* _collection; int32 _index; public: Iterator(Dictionary* collection, const int32 index) : _collection(collection) , _index(index) { } Iterator(Dictionary const* collection, const int32 index) : _collection(const_cast(collection)) , _index(index) { } Iterator() : _collection(nullptr) , _index(-1) { } Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) { } Iterator(Iterator&& i) : _collection(i._collection) , _index(i._index) { } public: FORCE_INLINE int32 Index() const { return _index; } 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) const { 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) const { 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 _allocation.Get()[pos.ObjectIndex].Value; // Insert ASSERT(pos.FreeSlotIndex != -1); Bucket& bucket = _allocation.Get()[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 _allocation.Get()[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 = _allocation.Get()[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 (ValueType*)&_allocation.Get()[pos.ObjectIndex].Value; } public: /// /// Clears the collection but without changing its capacity (all inserted elements: keys and values will be removed). /// void Clear() { if (_elementsCount + _deletedCount != 0) { Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) data[i].Free(); _elementsCount = _deletedCount = 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->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) { if (capacity == Capacity()) return; ASSERT(capacity >= 0); AllocationData oldAllocation; oldAllocation.Swap(_allocation); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _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].Key, MoveTemp(oldData[i].Value)); } } 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 (_size >= minCapacity) return; if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) minCapacity = DICTIONARY_DEFAULT_CAPACITY; const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); SetCapacity(capacity, preserveContents); } /// /// Swaps the contents of collection with the other object without copy operation. Performs fast internal data exchange. /// /// The other collection. void Swap(Dictionary& other) { ::Swap(_elementsCount, other._elementsCount); ::Swap(_deletedCount, other._deletedCount); ::Swap(_size, other._size); _allocation.Swap(other._allocation); } 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 = &_allocation.Get()[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 = &_allocation.Get()[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); const 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 false; FindPositionResult pos; FindPosition(key, pos); if (pos.ObjectIndex != -1) { _allocation.Get()[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(const Iterator& i) { ASSERT(i._collection == this); if (i) { ASSERT(_allocation.Get()[i._index].IsOccupied()); _allocation.Get()[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 (Iterator 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 (IsEmpty()) return End(); FindPositionResult pos; FindPosition(key, pos); return pos.ObjectIndex != -1 ? Iterator(this, pos.ObjectIndex) : 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()) { const Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) { if (data[i].IsOccupied() && data[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()) { const Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) { if (data[i].IsOccupied() && data[i].Value == value) { if (key) *key = data[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 (Iterator 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. template void GetKeys(Array& result) const { for (Iterator i = Begin(); i.IsNotEnd(); ++i) result.Add(i->Key); } /// /// Gets the values collection to the output array (may contain duplicates). /// /// The result. template void GetValues(Array& result) const { for (Iterator i = Begin(); i.IsNotEnd(); ++i) result.Add(i->Value); } 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 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(_size); const int32 tableSizeMinusOne = _size - 1; int32 bucketIndex = GetHash(key) & tableSizeMinusOne; int32 insertPos = -1; int32 checksCount = 0; const Bucket* data = _allocation.Get(); result.FreeSlotIndex = -1; while (checksCount < _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 key else if (bucket.Key == key) { // Found key result.ObjectIndex = bucketIndex; return; } checksCount++; bucketIndex = (bucketIndex + DICTIONARY_PROB_FUNC(_size, checksCount)) & tableSizeMinusOne; } result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } };