From a2151030028b85241f80e9ee1db15da647e5a25a Mon Sep 17 00:00:00 2001 From: Wiseguy <68165316+Mr-Wiseguy@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:33:09 -0400 Subject: [PATCH] Remove slotmap submodule and integrate header directly after submodule URL changed (#645) --- .gitmodules | 4 +- CMakeLists.txt | 2 +- lib/SlotMap/README.md | 257 ++++++++ lib/SlotMap/slot_map.h | 1366 ++++++++++++++++++++++++++++++++++++++++ lib/slot_map | 1 - 5 files changed, 1625 insertions(+), 5 deletions(-) create mode 100644 lib/SlotMap/README.md create mode 100644 lib/SlotMap/slot_map.h delete mode 160000 lib/slot_map diff --git a/.gitmodules b/.gitmodules index 61832bc..df5b4ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,6 +19,4 @@ [submodule "Zelda64RecompSyms"] path = Zelda64RecompSyms url = https://github.com/Zelda64Recomp/Zelda64RecompSyms -[submodule "lib/slot_map"] - path = lib/slot_map - url = https://github.com/SergeyMakeev/slot_map + diff --git a/CMakeLists.txt b/CMakeLists.txt index e00e20f..54a4949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,7 +222,7 @@ target_include_directories(Zelda64Recompiled PRIVATE ${CMAKE_SOURCE_DIR}/lib/rt64/src/render ${CMAKE_SOURCE_DIR}/lib/freetype-windows-binaries/include ${CMAKE_SOURCE_DIR}/lib/rt64/src/contrib/nativefiledialog-extended/src/include - ${CMAKE_SOURCE_DIR}/lib/slot_map/slot_map + ${CMAKE_SOURCE_DIR}/lib/SlotMap ${CMAKE_BINARY_DIR}/shaders ${CMAKE_CURRENT_BINARY_DIR} ) diff --git a/lib/SlotMap/README.md b/lib/SlotMap/README.md new file mode 100644 index 0000000..3bfe8bb --- /dev/null +++ b/lib/SlotMap/README.md @@ -0,0 +1,257 @@ +This file is originally from the repo: https://github.com/SergeyMakeev/SlotMap + +The original license and README are as follows: +``` +MIT License + +Copyright (c) 2022 Sergey Makeev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` +# Slot Map + +[![Actions Status](https://github.com/SergeyMakeev/slot_map/workflows/build/badge.svg)](https://github.com/SergeyMakeev/slot_map/actions) +[![Build status](https://ci.appveyor.com/api/projects/status/i00kv17e3ia5jr7q?svg=true)](https://ci.appveyor.com/project/SergeyMakeev/slot-map) +[![codecov](https://codecov.io/gh/SergeyMakeev/slot_map/branch/main/graph/badge.svg?token=3GRAFTRYQU)](https://codecov.io/gh/SergeyMakeev/slot_map) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) + +A Slot Map is a high-performance associative container with persistent unique keys to access stored values. Upon insertion, a key is returned that can be used to later access or remove the values. Insertion, removal, and access are all guaranteed to take `O(1)` time (best, worst, and average case) +Great for storing collections of objects that need stable, safe references but have no clear ownership. + +The difference between a `std::unordered_map` and a `dod::slot_map` is that the slot map generates and returns the key when inserting a value. A key is always unique and will only refer to the value that was inserted. + + Usage example: + ```cpp + slot_map strings; + auto red = strings.emplace("Red"); + auto green = strings.emplace("Green"); + auto blue = strings.emplace("Blue"); + + const std::string* val1 = strings.get(red); + if (val1) + { + printf("red = '%s'\n", val1->c_str()); + } + + strings.erase(green); + printf("%d\n", strings.has(green)); + printf("%d\n", strings.has(blue)); + ``` + + Output: + ``` + red = 'Red' + 0 + 1 + ``` + +# Implementation details + +The slot map container will allocate memory in pages (default page size = 4096 elements) to avoid memory spikes during growth and be able to deallocate pages that are no longer needed. +Also, the page-based memory allocator is very important since it guarantees "pointers stability"; hence, we never move values in memory. + + +Keys are always uses `uint64_t/uint32_t` (configurable) and technically typless, but we "artificially" make them typed to get a few extra compile-time checks. +i.e., the following code will produce a compiler error +```cpp +slot_map strings; +slot_map numbers; +slot_map::key numKey = numbers.emplace(3); +const std::string* value = strings.get(numKey); // <---- can not use slot_map::key to index slot_map ! +``` + +The keys can be converted to/from their numeric types if you do not need additional type checks. +```cpp +slot_map numbers; +slot_map::key numKey = numbers.emplace(3); +uint64_t rawKey = numKey; // convert to numeric type (like cast pointer to void*) +... +slot_map::key numKey2{rawKey}; // create key from numeric type +``` + +When a slot is reused, its version is automatically incremented (to invalidate all existing keys that refers to the same slot). +But since we only use 20-bits *(10-bits for 32 bit keys)* for version counter, there is a possibility that the version counter will wrap around, +and a new item will get the same key as a removed item. + +To mitigate this potential issue, once the version counter overflows, we disable that slot so that no new keys are returned for this slot +(this gives us a guarantee that there are no key collisions) + +To prevent version overflow from happening too often, we need to ensure that we don't reuse the same slot too often. +So we do not reuse recently freed slot-indices as long as their number is below a certain threshold (`kMinFreeIndices = 64`). + +Keys also can carry a few extra bits of information provided by a user that we called `tag`. +That might be handy to add application-specific data to keys. + +For example: +```cpp + slot_map strings; + auto red = strings.emplace("Red"); + red.set_tag(13); + + auto tag = red.get_tag(); + assert(tag == 13); +``` + +Here is how a key structure looks like internally + +64-bit key type + +| Component | Number of bits | +| ---------------|------------------------| +| tag | 12 | +| version | 20 (0..1,048,575 | +| index | 32 (0..4,294,967,295) | + +32-bit key type + +| Component | Number of bits | +| ---------------|---------------------| +| tag | 2 | +| version | 10 (0..1023) | +| index | 20 (0..1,048,575) | + +Note: To use your custom memory allocator define `SLOT_MAP_ALLOC`/`SLOT_MAP_FREE` before including `"slot_map.h"` + +```cpp +#define SLOT_MAP_ALLOC(sizeInBytes, alignment) aligned_alloc(alignment, sizeInBytes) +#define SLOT_MAP_FREE(ptr) free(ptr) +#include "slot_map.h" +``` + + +# API + +`bool has_key(key k) const noexcept` +Returns true if the slot map contains a specific key + +`void reset()` +Clears the slot map and releases any allocated memory. +Note: By calling this function, you must guarantee that no handles are in use! +Otherwise calling this function might be dangerous and lead to key "collisions". +You might consider using "clear()" instead. + +`void clear()` +Clears the slot map but keeps the allocated memory for reuse. +Automatically increases version for all the removed elements (the same as calling "erase()" for all existing elements) + +`const T* get(key k) const noexcept` +If key exists returns a const pointer to the value corresponding to the given key or returns null elsewere. + +`T* get(key k)` +If key exists returns a pointer to the value corresponding to the given key or returns null elsewere. + +`key emplace(Args&&... args)` +Constructs element in-place and returns a unique key that can be used to access this value. + +`void erase(key k)` +Removes element (if such key exists) from the slot map. + +`std::optional pop(key k)` +Removes element (if such key exists) from the slot map, returning the value at the key if the key was not previously removed. + +`bool empty() const noexcept` +Returns true if the slot map is empty. + +`size_type size() const noexcept` +Returns the number of elements in the slot map. + +`void swap(slot_map& other) noexcept` +Exchanges the content of the slot map by the content of another slot map object of the same type. + +`slot_map(const slot_map& other)` +Copy constructor + +`slot_map& operator=(const slot_map& other)` +Copy assignment + +`slot_map(slot_map&& other) noexcept` +Move constructor + +`slot_map& operator=(slot_map&& other) noexcept` +Move asignment + + +`const_values_iterator begin() const noexcept` +`const_values_iterator end() const noexcept` +Const values iterator + +```cpp +for (const auto& value : slotMap) +{ + ... +} +``` + +`Items items() const noexcept` +Const key/value iterator + +```cpp +for (const auto& [key, value] : slotMap.items()) +{ +... +} +``` + +# References + + Sean Middleditch + Data Structures for Game Developers: The Slot Map, 2013 + https://web.archive.org/web/20180121142549/http://seanmiddleditch.com/data-structures-for-game-developers-the-slot-map/ + + Niklas Gray + Building a Data-Oriented Entity System (part 1), 2014 + http://bitsquid.blogspot.com/2014/08/building-data-oriented-entity-system.html + + Noel Llopis + Managing Data Relationships, 2010 + https://gamesfromwithin.com/managing-data-relationships + + Stefan Reinalter + Adventures in data-oriented design - Part 3c: External References, 2013 + https://blog.molecular-matters.com/2013/07/24/adventures-in-data-oriented-design-part-3c-external-references/ + + Niklas Gray + Managing Decoupling Part 4 - The ID Lookup Table, 2011 + https://bitsquid.blogspot.com/2011/09/managing-decoupling-part-4-id-lookup.html + + Sander Mertens + Making the most of ECS identifiers, 2020 + https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647 + + Michele Caini + ECS back and forth. Part 9 - Sparse sets and EnTT, 2020 + https://skypjack.github.io/2020-08-02-ecs-baf-part-9/ + + Andre Weissflog + Handles are the better pointers, 2018 + https://floooh.github.io/2018/06/17/handles-vs-pointers.html + + Allan Deutsch + C++Now 2017: "The Slot Map Data Structure", 2017 + https://www.youtube.com/watch?v=SHaAR7XPtNU + + Jeff Gates + Init, Update, Draw - Data Arrays, 2012 + https://greysphere.tumblr.com/post/31601463396/data-arrays + + Niklas Gray + Data Structures Part 1: Bulk Data, 2019 + https://ourmachinery.com/post/data-structures-part-1-bulk-data/ + + diff --git a/lib/SlotMap/slot_map.h b/lib/SlotMap/slot_map.h new file mode 100644 index 0000000..467c01b --- /dev/null +++ b/lib/SlotMap/slot_map.h @@ -0,0 +1,1366 @@ +// This file is originally from the repo: https://github.com/SergeyMakeev/SlotMap. +// The original license and code are as follows: + +// MIT License +// +// Copyright (c) 2022 Sergey Makeev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +#include +#define PRIslotkey PRIu64 + +// TODO (detailed desc): you could override memory allocator by defining SLOT_MAP_ALLOC/SLOT_MAP_FREE macroses +#if !defined(SLOT_MAP_ALLOC) || !defined(SLOT_MAP_FREE) + +#if defined(_WIN32) +// Windows +#include +#define SLOT_MAP_ALLOC(sizeInBytes, alignment) _mm_malloc(sizeInBytes, alignment) +#define SLOT_MAP_FREE(ptr) _mm_free(ptr) +#else +// Posix +#include +#define SLOT_MAP_ALLOC(sizeInBytes, alignment) aligned_alloc(alignment, sizeInBytes) +#define SLOT_MAP_FREE(ptr) free(ptr) +#endif + +#endif + +// extern void _onAssertionFailed(const char* expression, const char* srcFile, unsigned int srcLine); +//#define SLOT_MAP_ASSERT(expression) (void)((!!(expression)) || (_onAssertionFailed(#expression, __FILE__, (unsigned int)(__LINE__)), 0)) + +// TODO (detailed desc): you could override asserts by defining SLOT_MAP_ASSERT macro +#if !defined(SLOT_MAP_ASSERT) +#include +#define SLOT_MAP_ASSERT(expression) assert(expression) +#endif + +namespace stl +{ +// STL compatible allocator +// Note: some platforms (macOS) does not support alignments smaller than `alignof(void*)` +template struct Allocator +{ + public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using size_type = size_t; + using difference_type = ptrdiff_t; + + template struct rebind + { + using other = Allocator; + }; + + Allocator() noexcept {} + Allocator(const Allocator& /*other*/) noexcept {} + + template Allocator(const Allocator& /* other */) noexcept {} + + ~Allocator() {} + + pointer address(reference x) const noexcept { return &x; } + const_pointer address(const_reference x) const noexcept { return &x; } + + pointer allocate(size_type n, [[maybe_unused]] const void* hint = 0) + { + size_t alignment = Alignment; + n = std::max(n, alignment); + pointer p = reinterpret_cast(SLOT_MAP_ALLOC((sizeof(value_type) * n), alignment)); + SLOT_MAP_ASSERT(p); + return p; + } + + void deallocate(pointer p, size_type /* n */) { SLOT_MAP_FREE(p); } + + size_type max_size() const noexcept { return std::numeric_limits::max() / sizeof(value_type); } + + template void construct(U* p, Args&&... args) + { + new (reinterpret_cast(p)) U(std::forward(args)...); + } + template void destroy(U* p) { p->~U(); } +}; + +template +bool operator==(const Allocator& /*lhs*/, const Allocator& /*rhs*/) noexcept +{ + return true; +} + +template +bool operator!=(const Allocator& /*lhs*/, const Allocator& /*rhs*/) noexcept +{ + return false; +} +} // namespace stl + +namespace dod +{ + +/* +Even though slot map keys are technically typeless (uint64_t), we artificially add a new type to get extra compiler checks. + +i.e., the following code should not compile +``` +slot_map strings; +slot_map numbers; +slot_map::key numKey = numbers.emplace(3); +const std::string* value = strings.get(numKey); +``` +*/ + +/* + +64-bit key + +| Component | Number of bits | +| ---------------|------------------------| +| tag | 12 | +| version | 20 (0..1,048,575 | +| index | 32 (0..4,294,967,295) | + +*/ +template struct slot_map_key64 +{ + using id_type = uint64_t; + using version_t = uint32_t; + using index_t = uint32_t; + using tag_t = uint16_t; + + static inline constexpr version_t kInvalidVersion = 0x0u; + static inline constexpr version_t kMinVersion = 0x1u; + static inline constexpr version_t kMaxVersion = 0x0fffffu; + static inline constexpr index_t kMaxIndex = 0xffffffffu; + static inline constexpr tag_t kMaxTag = 0x0fffu; + + static inline constexpr id_type kIndexMask = 0x00000000ffffffffull; + + static inline constexpr id_type kVersionMask = 0x0fffff00000000ull; + static inline constexpr id_type kVersionShift = 32ull; + + static inline constexpr id_type kHandleTagMask = 0xfff0000000000000ull; + static inline constexpr id_type kHandleTagShift = 52ull; + + static inline constexpr slot_map_key64 make(version_t version, index_t index) noexcept + { + SLOT_MAP_ASSERT(version != kInvalidVersion); + SLOT_MAP_ASSERT(index <= kMaxIndex); + id_type v = (static_cast(version) << kVersionShift) & kVersionMask; + id_type i = (static_cast(index)) & kIndexMask; + return slot_map_key64{v | i}; + } + + inline size_t hash() const noexcept { return std::hash{}(raw); } + + static inline slot_map_key64 clearTagAndUpdateVersion(slot_map_key64 key, version_t version) noexcept + { + SLOT_MAP_ASSERT(version != kInvalidVersion); + id_type ver = (static_cast(version) << kVersionShift) & kVersionMask; + return slot_map_key64{((key.raw & (~kVersionMask)) | ver) & (~kHandleTagMask)}; + } + static inline index_t toIndex(slot_map_key64 key) noexcept { return static_cast(key.raw & kIndexMask); } + static inline version_t toVersion(slot_map_key64 key) noexcept + { + return static_cast((key.raw & kVersionMask) >> kVersionShift); + } + static inline version_t increaseVersion(version_t version) noexcept { return (version + 1); } + + inline tag_t get_tag() const noexcept { return static_cast((raw & kHandleTagMask) >> kHandleTagShift); } + inline void set_tag(tag_t tag) noexcept + { + SLOT_MAP_ASSERT(tag <= kMaxTag); + id_type ud = (static_cast(tag) << kHandleTagShift) & kHandleTagMask; + raw = ((raw & (~kHandleTagMask)) | ud); + } + + slot_map_key64() noexcept = default; + explicit slot_map_key64(id_type raw) noexcept + : raw(raw) + { + } + slot_map_key64(const slot_map_key64&) noexcept = default; + slot_map_key64& operator=(const slot_map_key64&) noexcept = default; + slot_map_key64(slot_map_key64&&) noexcept = default; + slot_map_key64& operator=(slot_map_key64&&) noexcept = default; + + bool operator==(const slot_map_key64& other) const noexcept { return raw == other.raw; } + bool operator<(const slot_map_key64& other) const noexcept { return raw < other.raw; } + + // implicit conversion to id_type (useful for printing and debug) + operator id_type() const noexcept { return raw; } + + static inline slot_map_key64 invalid() noexcept { return slot_map_key64{0}; } + + id_type raw; +}; + +/* + +32-bit key + +| Component | Number of bits | +| ---------------|---------------------| +| tag | 2 | +| version | 10 (0..1023) | +| index | 20 (0..1,048,576) | + +*/ +template struct slot_map_key32 +{ + using id_type = uint32_t; + using version_t = uint16_t; + using index_t = uint32_t; + using tag_t = uint8_t; + + static inline constexpr version_t kInvalidVersion = 0x0u; + static inline constexpr version_t kMinVersion = 0x1u; + static inline constexpr version_t kMaxVersion = 0x0400u; + static inline constexpr index_t kMaxIndex = 0x000fffffu; + static inline constexpr tag_t kMaxTag = 0x03u; + + static inline constexpr id_type kIndexMask = 0x0fffffull; + + static inline constexpr id_type kVersionMask = 0x3ff00000ull; + static inline constexpr id_type kVersionShift = 20ull; + + static inline constexpr id_type kHandleTagMask = 0xc0000000ull; + static inline constexpr id_type kHandleTagShift = 30ull; + + static inline constexpr slot_map_key32 make(version_t version, index_t index) noexcept + { + SLOT_MAP_ASSERT(version != kInvalidVersion); + SLOT_MAP_ASSERT(index <= kMaxIndex); + id_type v = (static_cast(version) << kVersionShift) & kVersionMask; + id_type i = (static_cast(index)) & kIndexMask; + return slot_map_key32{v | i}; + } + + inline size_t hash() const noexcept { return std::hash{}(raw); } + + static inline slot_map_key32 clearTagAndUpdateVersion(slot_map_key32 key, version_t version) noexcept + { + SLOT_MAP_ASSERT(version != kInvalidVersion); + id_type ver = (static_cast(version) << kVersionShift) & kVersionMask; + return slot_map_key32{((key.raw & (~kVersionMask)) | ver) & (~kHandleTagMask)}; + } + static inline index_t toIndex(slot_map_key32 key) noexcept { return static_cast(key.raw & kIndexMask); } + static inline version_t toVersion(slot_map_key32 key) noexcept + { + return static_cast((key.raw & kVersionMask) >> kVersionShift); + } + static inline version_t increaseVersion(version_t version) noexcept { return (version + 1); } + + inline tag_t get_tag() const noexcept { return static_cast((raw & kHandleTagMask) >> kHandleTagShift); } + inline void set_tag(tag_t tag) noexcept + { + SLOT_MAP_ASSERT(tag <= kMaxTag); + id_type ud = (static_cast(tag) << kHandleTagShift) & kHandleTagMask; + raw = ((raw & (~kHandleTagMask)) | ud); + } + + slot_map_key32() noexcept = default; + explicit slot_map_key32(id_type raw) noexcept + : raw(raw) + { + } + slot_map_key32(const slot_map_key32&) noexcept = default; + slot_map_key32& operator=(const slot_map_key32&) noexcept = default; + slot_map_key32(slot_map_key32&&) noexcept = default; + slot_map_key32& operator=(slot_map_key32&&) noexcept = default; + + bool operator==(const slot_map_key32& other) const noexcept { return raw == other.raw; } + bool operator<(const slot_map_key32& other) const noexcept { return raw < other.raw; } + + // implicit conversion to id_type (useful for printing and debug) + operator id_type() const noexcept { return raw; } + + static inline slot_map_key32 invalid() noexcept { return slot_map_key32{0}; } + + id_type raw; +}; + +/* + A slot map is a high-performance associative container with persistent unique keys to access stored values. Upon insertion, a key is + returned that can be used to later access or remove the values. Insertion, removal, and access are all guaranteed to take O(1) time (best, + worst, and average case) Great for storing collections of objects that need stable, safe references but have no clear ownership. + + The difference between a std::unordered_map and a slot map is that the slot map generates and returns the key when inserting a value. A + key is always unique and will only refer to the value that was inserted. + + Usage example: + ``` + slot_map strings; + auto red = strings.emplace("Red"); + auto green = strings.emplace("Green"); + auto blue = strings.emplace("Blue"); + + const std::string* val1 = strings.get(red); + if (val1) + { + printf("red = '%s'\n", val1->c_str()); + } + + strings.erase(green); + printf("%d\n", strings.has(green)); + printf("%d\n", strings.has(blue)); + ``` + + Output: + ``` + red = 'Red' + 0 + 1 + ``` + +*/ + +/* + Usefull reading: + + Niklas Gray + Building a Data-Oriented Entity System (part 1), 2014 + http://bitsquid.blogspot.com/2014/08/building-data-oriented-entity-system.html + + Noel Llopis + Managing Data Relationships, 2010 + https://gamesfromwithin.com/managing-data-relationships + + Stefan Reinalter + Adventures in data-oriented design - Part 3c: External References, 2013 + https://blog.molecular-matters.com/2013/07/24/adventures-in-data-oriented-design-part-3c-external-references/ + + Niklas Gray + Managing Decoupling Part 4 - The ID Lookup Table, 2011 + https://bitsquid.blogspot.com/2011/09/managing-decoupling-part-4-id-lookup.html + + Sander Mertens + Making the most of ECS identifiers, 2020 + https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647 + + Michele Caini + ECS back and forth. Part 9 - Sparse sets and EnTT, 2020 + https://skypjack.github.io/2020-08-02-ecs-baf-part-9/ + + Andre Weissflog + Handles are the better pointers, 2018 + https://floooh.github.io/2018/06/17/handles-vs-pointers.html + + Allan Deutsch + C++Now 2017: "The Slot Map Data Structure", 2017 + https://www.youtube.com/watch?v=SHaAR7XPtNU + + Jeff Gates + Init, Update, Draw - Data Arrays, 2012 + https://greysphere.tumblr.com/post/31601463396/data-arrays +*/ +template , size_t PAGESIZE = 4096, size_t MINFREEINDICES = 64> class slot_map +{ + public: + using key = TKeyType; + using version_t = typename TKeyType::version_t; + using index_t = typename TKeyType::index_t; + using tag_t = typename TKeyType::tag_t; + using size_type = uint32_t; + + /* + kPageSize = 4096 (default) + + Page size in elements. When all page items(slots) are disabled we automatically release that page to save memory. + The page-based allocator also helps with pointers' stability and prevents memory spikes on reallocation. + + 4096 seems like a reasonable number of items per page. + I.e., for simple uint32_t type, it will take 16Kb of RAM per page = good compromise between CPU cache utilization, + allocation granularity, and memory overhead from deactivated slots. + */ + static inline constexpr size_type kPageSize = static_cast(PAGESIZE); + + /* + kMinFreeIndices = 64 (default) + + When a slot is reused, its version is automatically incremented (to make existing keys invalid). + But since we only use 16-bits for version numbers, there is a possibility that the version counter will wrap around, + and a new item will get the same key as a removed item. + + Once the version counter overflows, we disable that slot so that no new keys are returned for this slot + (this gives us a guarantee that there are no key collisions) + + To prevent version overflow from happening too often, we need to ensure that we don't reuse the same slot too often. + So we do not reuse indices as long as their number is below a certain threshold. + */ + static inline constexpr size_type kMinFreeIndices = static_cast(MINFREEINDICES); + + private: + using ValueStorage = typename std::aligned_storage::type; + + struct Meta + { + version_t version; // note: 0 is reserved for kInvalidVersion + uint8_t tombstone; // note: we only need 1 bit for tombstone marker + uint8_t inactive; // note: we only need 1 bit for inactive marker + }; + + struct Page + { + void* rawMemory; + ValueStorage* values; + Meta* meta; + size_type numInactiveSlots; + size_type numUsedElements; + + Page() noexcept + : rawMemory(nullptr) + , values(nullptr) + , meta(nullptr) + , numInactiveSlots(0) + , numUsedElements(0) + { + } + + Page(const Page&) = delete; + Page& operator=(const Page&) = delete; + Page& operator=(Page&&) = delete; + Page(Page&& other) noexcept + : rawMemory(nullptr) + , values(nullptr) + , meta(nullptr) + , numInactiveSlots(0) + , numUsedElements(0) + { + std::swap(rawMemory, other.rawMemory); + std::swap(meta, other.meta); + std::swap(values, other.values); + std::swap(numInactiveSlots, other.numInactiveSlots); + std::swap(numUsedElements, other.numUsedElements); + } + ~Page() { deallocate(); } + + void deallocate() + { + if (!rawMemory) + { + SLOT_MAP_ASSERT(!values); + SLOT_MAP_ASSERT(!meta); + return; + } + SLOT_MAP_ASSERT(values); + SLOT_MAP_ASSERT(meta); + + SLOT_MAP_FREE(rawMemory); + rawMemory = nullptr; + values = nullptr; + meta = nullptr; + } + + void allocate() + { + SLOT_MAP_ASSERT(!rawMemory); + SLOT_MAP_ASSERT(!values); + SLOT_MAP_ASSERT(!meta); + + size_type metaSize = static_cast(sizeof(Meta)) * kPageSize; + size_type dataSize = static_cast(sizeof(ValueStorage)) * kPageSize; + size_type alignedDataSize = align(dataSize, static_cast(alignof(Meta))); + size_type alignment = std::max(static_cast(alignof(Meta)), static_cast(alignof(ValueStorage))); + // some platforms (macOS) does not support alignments smaller than `alignof(void*)` + // and 16 bytes seem like a nice compromise + alignment = std::max(alignment, 16u); + size_type numBytes = alignedDataSize + metaSize; + + /* + C++11 std::aligned_alloc + + Passing a size which is not an integral multiple of alignment or an alignment which is not valid or not supported by the + implementation causes the function to fail and return a null pointer (C11, as published, specified undefined behavior in + this case, this was corrected by DR 460) + */ + numBytes = align(numBytes, alignment); + SLOT_MAP_ASSERT((numBytes % alignment) == 0); + rawMemory = SLOT_MAP_ALLOC(static_cast(numBytes), static_cast(alignment)); + SLOT_MAP_ASSERT(rawMemory); + + numInactiveSlots = 0; + numUsedElements = 0; + values = reinterpret_cast(rawMemory); + meta = reinterpret_cast(reinterpret_cast(rawMemory) + alignedDataSize); + + // TODO: remove rawMemory member + SLOT_MAP_ASSERT(values == rawMemory); + SLOT_MAP_ASSERT(values); + SLOT_MAP_ASSERT(meta); + SLOT_MAP_ASSERT(isPointerAligned(values, alignof(ValueStorage))); + SLOT_MAP_ASSERT(isPointerAligned(meta, alignof(Meta))); + } + }; + + static inline size_type align(size_type cursor, size_type alignment) noexcept { return (cursor + (alignment - 1)) & ~(alignment - 1); } + static inline bool isPointerAligned(void* cursor, size_t alignment) noexcept { return (uintptr_t(cursor) & (alignment - 1)) == 0; } + + template static void construct(void* mem, Args&&... args) + { + new (mem) TYPE(std::forward(args)...); + } + template void destruct(TYPE* p) { p->~T(); } + + const T* getImpl(key k) const noexcept + { + index_t index = key::toIndex(k); + if (index > getMaxValidIndex()) + { + // index out of bounds + return nullptr; + } + + PageAddr addr = getAddrFromIndex(index); + if (!isActivePage(addr)) + { + return nullptr; + } + + const Meta& m = getMetaByAddr(addr); + + version_t version = key::toVersion(k); + if (m.version != version) + { + // version mismatch, slot has been reused + return nullptr; + } + SLOT_MAP_ASSERT(m.version != key::kInvalidVersion); + SLOT_MAP_ASSERT(m.tombstone == 0); + SLOT_MAP_ASSERT(m.inactive == 0); + + const ValueStorage& v = getValueByAddr(addr); + const T* value = reinterpret_cast(&v); + return value; + } + + index_t appendElement() + { + if (pages.empty() || pages.back().numUsedElements == kPageSize) + { + Page& p = pages.emplace_back(); + p.allocate(); + } + + Page& lastPage = pages.back(); + + size_type elementIndex = lastPage.numUsedElements; + SLOT_MAP_ASSERT(elementIndex <= kPageSize); + lastPage.numUsedElements++; + + Meta& m = lastPage.meta[elementIndex]; + m.version = key::kMinVersion; + m.tombstone = 0; + m.inactive = 0; + + SLOT_MAP_ASSERT(pages.size() >= 1); + index_t index = static_cast(getIndexFromAddr(PageAddr{static_cast(pages.size()) - 1, elementIndex})); + return index; + } + + struct PageAddr + { + size_type page; + size_type index; + }; + + template inline static constexpr bool isPow2(INTEGRAL_TYPE x) noexcept + { + static_assert(std::is_integral::value, "isPow2 must be called on an integer type."); + return (x & (x - 1)) == 0 && (x != 0); + } + + static inline PageAddr getAddrFromIndex(size_type index) noexcept + { + static_assert(isPow2(kPageSize), "kPageSize is expected to be a power of two."); + PageAddr res; + res.page = index / kPageSize; + res.index = index % kPageSize; + return res; + } + + // to global index + static inline index_t getIndexFromAddr(PageAddr addr) noexcept + { + size_type index = (addr.page * kPageSize + addr.index); + return index_t(index); + } + + const Meta& getMetaByAddrImpl(PageAddr addr) const noexcept + { + SLOT_MAP_ASSERT(addr.page < pages.size()); + const Page& page = pages[addr.page]; + SLOT_MAP_ASSERT(page.meta); + SLOT_MAP_ASSERT(addr.index < kPageSize); + return page.meta[addr.index]; + } + + const ValueStorage& getValueByAddrImpl(PageAddr addr) const noexcept + { + SLOT_MAP_ASSERT(addr.page < pages.size()); + const Page& page = pages[addr.page]; + SLOT_MAP_ASSERT(page.values); + SLOT_MAP_ASSERT(addr.index < kPageSize); + return page.values[addr.index]; + } + + const Meta& getMetaByAddr(PageAddr addr) const noexcept { return getMetaByAddrImpl(addr); } + const ValueStorage& getValueByAddr(PageAddr addr) const noexcept { return getValueByAddrImpl(addr); } + + Meta& getMetaByAddr(PageAddr addr) noexcept + { + const Meta& constRes = getMetaByAddrImpl(addr); + return const_cast(constRes); + } + + ValueStorage& getValueByAddr(PageAddr addr) noexcept + { + const ValueStorage& constRes = getValueByAddrImpl(addr); + return const_cast(constRes); + } + + bool isTombstone(size_type index) const noexcept + { + PageAddr addr = getAddrFromIndex(index); + SLOT_MAP_ASSERT(addr.page < pages.size()); + const Page& page = pages[addr.page]; + if (!page.meta) + { + return true; + } + SLOT_MAP_ASSERT(addr.index < kPageSize); + return (page.meta[addr.index].tombstone != 0); + } + + bool isActivePage(PageAddr addr) const noexcept + { + if (addr.page >= pages.size()) + { + return false; + } + const Page& page = pages[addr.page]; + return (page.meta != nullptr); + } + + size_type getMaxValidIndex() const noexcept { return maxValidIndex; } + + void copyFrom(const slot_map& other) + { + reset(); + + SLOT_MAP_ASSERT(numItems == 0); + SLOT_MAP_ASSERT(maxValidIndex == 0); + SLOT_MAP_ASSERT(pages.empty()); + SLOT_MAP_ASSERT(freeIndices.empty()); + + static_assert(std::is_standard_layout::value && std::is_trivially_copyable::value, + "Meta is expected to be memcopyable (POD type)"); + + numItems = other.numItems; + maxValidIndex = other.maxValidIndex; + + for (size_t pageIndex = 0; pageIndex < other.pages.size(); pageIndex++) + { + const Page& otherPage = other.pages[pageIndex]; + if (otherPage.meta) + { + SLOT_MAP_ASSERT(otherPage.values); + + // active page + Page& p = pages.emplace_back(); + p.allocate(); + p.numInactiveSlots = otherPage.numInactiveSlots; + p.numUsedElements = otherPage.numUsedElements; + + // copy meta + size_type metaSize = static_cast(sizeof(Meta)) * kPageSize; + std::memcpy(p.meta, otherPage.meta, metaSize); + + // copy data + if constexpr (std::is_standard_layout::value && std::is_trivially_copyable::value) + { + size_type dataSize = static_cast(sizeof(ValueStorage)) * kPageSize; + std::memcpy(p.values, otherPage.values, dataSize); + } + else + { + for (size_type elementIndex = 0; elementIndex < p.numUsedElements; elementIndex++) + { + PageAddr addr; + addr.page = static_cast(pageIndex); + addr.index = elementIndex; + + const ValueStorage& otherVal = other.getValueByAddr(addr); + const T* otherV = reinterpret_cast(&otherVal); + ValueStorage& val = getValueByAddr(addr); + + // copy constructor + construct(&val, *otherV); + } + } + } + else + { + SLOT_MAP_ASSERT(otherPage.meta == nullptr); + SLOT_MAP_ASSERT(otherPage.values == nullptr); + + // inactive page + Page& p = pages.emplace_back(); + p.numInactiveSlots = otherPage.numInactiveSlots; + p.numUsedElements = otherPage.numUsedElements; + SLOT_MAP_ASSERT(p.values == nullptr); + SLOT_MAP_ASSERT(p.meta == nullptr); + } + } + } + + void callDtors() + { + size_type numItemsDestroyed = 0; + for (size_t pageIndex = 0; pageIndex < pages.size(); pageIndex++) + { + Page& page = pages[pageIndex]; + if (page.meta == nullptr) + { + continue; + } + for (size_type elementIndex = 0; elementIndex < page.numUsedElements; elementIndex++) + { + PageAddr addr; + addr.page = static_cast(pageIndex); + addr.index = static_cast(elementIndex); + + Meta& m = getMetaByAddr(addr); + if (m.tombstone != 0) + { + continue; + } + if constexpr (!std::is_trivially_destructible::value) + { + ValueStorage& v = getValueByAddr(addr); + destruct(reinterpret_cast(&v)); + } + numItemsDestroyed++; + } + } + SLOT_MAP_ASSERT(numItemsDestroyed == numItems); + } + + enum class EraseResult + { + NotFound, + ErasedAndIndexRecycled, + ErasedAndPageDeactivated, + }; + + template EraseResult eraseImpl(key k) + { + index_t index = key::toIndex(k); + if (index > getMaxValidIndex()) + { + return EraseResult::NotFound; + } + + PageAddr addr = getAddrFromIndex(index); + if (!isActivePage(addr)) + { + return EraseResult::NotFound; + } + + Meta& m = getMetaByAddr(addr); + if (m.tombstone != 0) + { + return EraseResult::NotFound; + } + + version_t slotVersion = m.version; + + if constexpr (VERSION_CHECK) + { + version_t version = key::toVersion(k); + if (slotVersion != version || slotVersion == key::kInvalidVersion || version == key::kInvalidVersion) + { + return EraseResult::NotFound; + } + } + + bool deactivateSlot = (slotVersion == key::kMaxVersion); + if (deactivateSlot) + { + // version overflow = deactivate slot + m.inactive = 1; + } + else + { + // increase version + slotVersion = key::increaseVersion(slotVersion); + SLOT_MAP_ASSERT(slotVersion != key::kInvalidVersion); + SLOT_MAP_ASSERT(slotVersion > m.version); + } + + m.version = slotVersion; + m.tombstone = 1; + + if constexpr (!std::is_trivially_destructible::value) + { + ValueStorage& v = getValueByAddr(addr); + const T* pv = reinterpret_cast(&v); + destruct(pv); + } + numItems--; + + if (deactivateSlot) + { + Page& page = pages[addr.page]; + page.numInactiveSlots++; + if (page.numInactiveSlots == kPageSize) + { + page.deallocate(); + return EraseResult::ErasedAndPageDeactivated; + } + } + else + { + // recycle index id (note: tag is not saved!) + freeIndices.emplace_back(key::clearTagAndUpdateVersion(k, slotVersion)); + } + return EraseResult::ErasedAndIndexRecycled; + } + + public: + slot_map() + : numItems(0) + , maxValidIndex(0) + { + } + ~slot_map() { callDtors(); } + + /* + Returns true if the slot map contains a specific key + */ + bool has_key(key k) const noexcept + { + index_t index = key::toIndex(k); + if (index > getMaxValidIndex()) + { + return false; + } + version_t version = key::toVersion(k); + PageAddr addr = getAddrFromIndex(index); + if (!isActivePage(addr)) + { + return false; + } + const Meta& m = getMetaByAddr(addr); + return (m.version == version); + } + + /* + Clears the slot map and releases any allocated memory. + Note: By calling this function, you must guarantee that no handles are in use! + Otherwise calling this function might be dangerous and lead to key "collisions". + You might consider using "clear()" instead. + */ + void reset() + { + callDtors(); + + numItems = 0; + maxValidIndex = 0; + + // Release used memory (using swap trick) + if (!pages.empty()) + { + std::vector> tmpPages; + pages.swap(tmpPages); + } + + if (!freeIndices.empty()) + { + std::deque> tmpFreeIndices; + freeIndices.swap(tmpFreeIndices); + } + } + + /* + Clears the slot map but keeps the allocated memory for reuse. + Automatically increases version for all the removed elements (the same as calling "erase()" for all existing elements) + */ + void clear() + { + for (size_t pageIndex = 0; pageIndex < pages.size(); pageIndex++) + { + Page& page = pages[pageIndex]; + if (page.meta == nullptr) + { + continue; + } + for (size_type elementIndex = 0; elementIndex < page.numUsedElements; elementIndex++) + { + index_t index = getIndexFromAddr(PageAddr{static_cast(pageIndex), elementIndex}); + // note: version doesn't matter here + EraseResult res = eraseImpl(key::make(key::kMinVersion, index)); + if (res == EraseResult::ErasedAndPageDeactivated) + { + // noting left on this page - go to the next page + break; + } + } + } + SLOT_MAP_ASSERT(numItems == 0); + } + + /* + If key exists returns a const pointer to the value corresponding to the given key or returns null elsewere. + */ + const T* get(key k) const noexcept { return getImpl(k); } + + /* + If key exists returns a pointer to the value corresponding to the given key or returns null elsewere. + */ + T* get(key k) noexcept + { + const T* constRes = getImpl(k); + return const_cast(constRes); + } + + /* + Constructs element in-place and returns a unique key that can be used to access this value. + */ + template key emplace(Args&&... args) + { + // Use recycled IDs only if we accumulated enough of them + if (static_cast(freeIndices.size()) > kMinFreeIndices) + { + key k = freeIndices.front(); + freeIndices.pop_front(); + + index_t index = key::toIndex(k); + SLOT_MAP_ASSERT(index <= getMaxValidIndex()); + + PageAddr addr = getAddrFromIndex(index); + Meta& m = getMetaByAddr(addr); + SLOT_MAP_ASSERT(m.inactive == 0); + SLOT_MAP_ASSERT(m.tombstone != 0); + SLOT_MAP_ASSERT(k.get_tag() == 0); + + m.tombstone = 0; + + ValueStorage& v = getValueByAddr(addr); + construct(&v, std::forward(args)...); + numItems++; + return k; + } + + // allocate new item + index_t index = appendElement(); + maxValidIndex = std::max(maxValidIndex, index); + + PageAddr addr = getAddrFromIndex(index); + const Meta& m = getMetaByAddr(addr); + SLOT_MAP_ASSERT(m.tombstone == 0); + + ValueStorage& v = getValueByAddr(addr); + construct(&v, std::forward(args)...); + numItems++; + key k = key::make(m.version, index); + return k; + } + + /* + Removes element (if such key exists) from the slot map. + */ + void erase(key k) { eraseImpl(k); } + + /* + Removes element (if such key exists) from the slot map, returning the value at the key if the key was not previously removed. + */ + std::optional pop(key k) + { + T* val = get(k); + if (val == nullptr) + { + return {}; + } + + T res(std::move(*val)); + eraseImpl(k); + return res; + } + + /* + Returns true if the slot map is empty. + */ + bool empty() const noexcept { return (numItems == 0); } + + /* + Returns the number of elements in the slot map. + */ + size_type size() const noexcept { return numItems; } + + /* + Exchanges the content of the slot map by the content of another slot map object of the same type. + */ + void swap(slot_map& other) noexcept + { + pages.swap(other.pages); + freeIndices.swap(other.freeIndices); + std::swap(numItems, other.numItems); + std::swap(maxValidIndex, other.maxValidIndex); + } + + // copy constructor + slot_map(const slot_map& other) + : numItems(0) + , maxValidIndex(0) + { + copyFrom(other); + } + + // copy assignment + slot_map& operator=(const slot_map& other) + { + copyFrom(other); + return *this; + } + + // move constructor + slot_map(slot_map&& other) noexcept + : numItems(other.numItems) + , maxValidIndex(other.maxValidIndex) + { + std::swap(pages, other.pages); + std::swap(freeIndices, other.freeIndices); + other.numItems = 0; + other.maxValidIndex = 0; + } + + // move asignment + slot_map& operator=(slot_map&& other) noexcept + { + // reset and swap + reset(); + + pages.swap(other.pages); + freeIndices.swap(other.freeIndices); + std::swap(numItems, other.numItems); + std::swap(maxValidIndex, other.maxValidIndex); + return *this; + } + + struct Stats + { + size_type numPagesTotal = 0; + size_type numInactivePages = 0; + size_type numActivePages = 0; + + size_type numItemsTotal = 0; + size_type numAliveItems = 0; + size_type numTombstoneItems = 0; + size_type numInactiveItems = 0; + }; + + /* + Returns the internal stats for debug purposes + + Note: O(n) complexity! + */ + Stats debug_stats() const noexcept + { + Stats stats; + stats.numPagesTotal = static_cast(pages.size()); + + for (size_t pageIndex = 0; pageIndex < pages.size(); pageIndex++) + { + const Page& page = pages[pageIndex]; + if (page.meta == nullptr) + { + stats.numInactivePages++; + continue; + } + stats.numActivePages++; + + stats.numItemsTotal += page.numUsedElements; + for (size_type elementIndex = 0; elementIndex < page.numUsedElements; elementIndex++) + { + PageAddr addr; + addr.page = static_cast(pageIndex); + addr.index = static_cast(elementIndex); + const Meta& m = getMetaByAddr(addr); + if (m.inactive != 0) + { + stats.numInactiveItems++; + } + else if (m.tombstone != 0) + { + stats.numTombstoneItems++; + } + else + { + stats.numAliveItems++; + } + } + } + return stats; + } + + public: + // iterators..... + + // for(const auto& kv : slot_map.items()) {} // key/value + // for(const auto& v : slot_map) {} // values only + + // values iterator................................... + class const_values_iterator + { + private: + const T* getCurrent() const noexcept + { + SLOT_MAP_ASSERT(currentIndex <= slotMap->getMaxValidIndex()); + PageAddr addr = slotMap->getAddrFromIndex(currentIndex); + SLOT_MAP_ASSERT(slotMap->isActivePage(addr)); + const ValueStorage& v = slotMap->getValueByAddr(addr); + const T* value = reinterpret_cast(&v); + return value; + } + + public: + explicit const_values_iterator(const slot_map* _slotMap, size_type index) noexcept + : slotMap(_slotMap) + , currentIndex(index) + { + } + + const T& operator*() const noexcept { return *getCurrent(); } + const T* operator->() const noexcept { return getCurrent(); } + + bool operator==(const const_values_iterator& other) const noexcept + { + return slotMap == other.slotMap && currentIndex == other.currentIndex; + } + bool operator!=(const const_values_iterator& other) const noexcept + { + return slotMap != other.slotMap || currentIndex != other.currentIndex; + } + + const_values_iterator& operator++() noexcept + { + do + { + currentIndex++; + } while (currentIndex <= slotMap->getMaxValidIndex() && slotMap->isTombstone(currentIndex)); + return *this; + } + + const_values_iterator operator++(int) noexcept + { + const_values_iterator res = *this; + ++*this; + return res; + } + + private: + const slot_map* slotMap; + size_type currentIndex; + }; + + const_values_iterator begin() const noexcept + { + if (pages.empty()) return end(); + + size_type index = 0; + while (index <= getMaxValidIndex() && isTombstone(index)) + { + index++; + } + return const_values_iterator(this, index); + } + const_values_iterator end() const noexcept { return const_values_iterator(this, getMaxValidIndex() + static_cast(1)); } + + // key-value iterator................................... + class const_kv_iterator + { + public: + // pretty much similar to std::reference_wrapper + template struct reference + { + TYPE* ptr = nullptr; + + explicit reference(TYPE* _ptr) + : ptr(_ptr) + { + } + + reference(const reference& /*other*/) noexcept = default; + reference(reference&& /*other*/) noexcept = default; + reference& operator=(const reference& /*other*/) noexcept = default; + reference& operator=(reference&& /*other*/) noexcept = default; + + void set(TYPE* _ptr) noexcept { ptr = _ptr; } + + TYPE& get() const noexcept + { + SLOT_MAP_ASSERT(ptr); + return *ptr; + } + + operator TYPE&() const noexcept { return get(); } + }; + + using KeyValue = std::pair>; + + private: + void updateTmpKV() const noexcept + { + SLOT_MAP_ASSERT(currentIndex <= slotMap->getMaxValidIndex()); + PageAddr addr = slotMap->getAddrFromIndex(currentIndex); + SLOT_MAP_ASSERT(slotMap->isActivePage(addr)); + const Meta& m = slotMap->getMetaByAddr(addr); + const ValueStorage& v = slotMap->getValueByAddr(addr); + const T* value = reinterpret_cast(&v); + tmpKv.first = key::make(m.version, index_t(currentIndex)); + const reference& ref = tmpKv.second; + const_cast&>(ref).set(value); + } + + public: + explicit const_kv_iterator(const slot_map* _slotMap, size_type index) noexcept + : slotMap(_slotMap) + , currentIndex(index) + , tmpKv(key::invalid(), reference(nullptr)) + { + } + + const KeyValue& operator*() const noexcept + { + updateTmpKV(); + return tmpKv; + } + + const KeyValue* operator->() const noexcept + { + updateTmpKV(); + return &tmpKv; + } + + bool operator==(const const_kv_iterator& other) const noexcept + { + return slotMap == other.slotMap && currentIndex == other.currentIndex; + } + bool operator!=(const const_kv_iterator& other) const noexcept + { + return slotMap != other.slotMap || currentIndex != other.currentIndex; + } + + const_kv_iterator& operator++() noexcept + { + do + { + currentIndex++; + } while (currentIndex <= slotMap->getMaxValidIndex() && slotMap->isTombstone(currentIndex)); + return *this; + } + + const_kv_iterator operator++(int) noexcept + { + const_kv_iterator res = *this; + ++*this; + return res; + } + + private: + const slot_map* slotMap; + size_type currentIndex; + // unfortunately we need this to make it look like standard STL iterator (operator* and operator->) + mutable KeyValue tmpKv; + }; + + // proxy items object + class Items + { + const slot_map* slotMap; + + public: + explicit Items(const slot_map* _slotMap) noexcept + : slotMap(_slotMap) + { + } + + const_kv_iterator begin() const noexcept + { + if (slotMap->pages.empty()) return end(); + size_type index = 0; + while (index <= slotMap->getMaxValidIndex() && slotMap->isTombstone(index)) + { + index++; + } + return const_kv_iterator(slotMap, index); + } + const_kv_iterator end() const noexcept + { + return const_kv_iterator(slotMap, slotMap->getMaxValidIndex() + static_cast(1)); + } + }; + + Items items() const noexcept { return Items(this); } + + private: + std::vector> pages; + std::deque> freeIndices; + size_type numItems; + index_t maxValidIndex; +}; + +template +using slot_map32 = slot_map, PAGESIZE, MINFREEINDICES>; + +template +using slot_map64 = slot_map, PAGESIZE, MINFREEINDICES>; + +} // namespace dod + +// std::hash support +namespace std +{ +template struct hash> +{ + size_t operator()(const typename dod::slot_map_key64& key) const noexcept { return key.hash(); } +}; + +template struct hash> +{ + size_t operator()(const typename dod::slot_map_key32& key) const noexcept { return key.hash(); } +}; + +} // namespace std diff --git a/lib/slot_map b/lib/slot_map deleted file mode 160000 index b8ac8eb..0000000 --- a/lib/slot_map +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b8ac8ebd89aa1cd18f20ce6e4ad1cac716f1933f