/* * flat_handle_map * * A container which provides stable handles to inserted elements. Elements are accessed and erased * in constant time from handles. Insert and emplace occur in amortized constant time. * * Handle map operates similar to std::map, however, elements are stored in a vector-like * underlying container. Handles and iterators remain valid even when other elements are added or * erased, but valid elements are not necessarily stored contiguously. References to elements are * invalidated when inserting into the container, but not when erasing. * * Internally, a list of occupied elements are kept. When an element is erased, it is destroyed and * marked as available. During insertion, the first available element is re-used. If they are all * taken, the underlying container size is increased. * * MIT License * * Copyright (c) 2022 Michael R. P. Ragazzon * * 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 template > struct flat_handle_map { public: enum class handle : size_t {}; class const_iterator { private: const flat_handle_map& container; size_t i; public: using iterator_category = std::forward_iterator_tag; using value_type = T; using difference_type = std::ptrdiff_t; using pointer = const T*; using reference = const T&; explicit const_iterator(const flat_handle_map& container, size_t i) : container(container), i(i) {} reference operator*() const { return *(container.data() + i); } bool operator==(const const_iterator& other) const { return i == other.i; } bool operator!=(const const_iterator& other) const { return !(*this == other); } const_iterator operator++(int) { const_iterator retval = *this; ++(*this); return retval; } const_iterator& operator++() { ++i; i = container.get_next_occupied(i); return *this; } handle get_handle() const { return static_cast(i); } }; class iterator { private: flat_handle_map& container; size_t i; public: using iterator_category = std::forward_iterator_tag; using value_type = T; using difference_type = std::ptrdiff_t; using pointer = T*; using reference = T&; explicit iterator(flat_handle_map& container, size_t i) : container(container), i(i) {} reference operator*() const { return *(container.data() + i); } bool operator==(const iterator& other) const { return i == other.i; } bool operator!=(const iterator& other) const { return !(*this == other); } iterator operator++(int) { iterator retval = *this; ++(*this); return retval; } iterator& operator++() { ++i; i = container.get_next_occupied(i); return *this; } operator const_iterator() const { return const_iterator{container, i}; } handle get_handle() const { return static_cast(i); } }; using AllocatorTraits = std::allocator_traits; using allocator_type = Alloc; using value_type = typename AllocatorTraits::value_type; using size_type = typename AllocatorTraits::size_type; using difference_type = typename AllocatorTraits::difference_type; using reference = typename value_type&; using const_reference = typename const value_type&; using pointer = typename AllocatorTraits::pointer; using const_pointer = typename AllocatorTraits::const_pointer; flat_handle_map() : flat_handle_map(Alloc()) {} flat_handle_map(const Alloc& alloc) : m_capacity(0), m_alloc(alloc) { m_begin = m_end = nullptr; } explicit flat_handle_map(size_t count, const T& value, const Alloc& alloc = Alloc()) : flat_handle_map(alloc) { assign_impl(count, value); } template ())> flat_handle_map(InputIterator first, InputIterator last, const Alloc& alloc = Alloc()) : flat_handle_map(alloc) { assign_impl(first, last); } flat_handle_map(std::initializer_list l, const Alloc& alloc = Alloc()) : flat_handle_map(alloc) { assign_impl(l); } flat_handle_map(const flat_handle_map& v) : flat_handle_map(v, std::allocator_traits::select_on_container_copy_construction(v.get_allocator())) {} flat_handle_map(const flat_handle_map& v, const Alloc& alloc) : flat_handle_map(alloc) { assign_impl(v); } flat_handle_map(flat_handle_map&& v) : m_capacity(v.m_capacity), m_alloc(std::move(v.m_alloc)), m_begin(v.m_begin), m_end(v.m_end), m_occupied(std::move(v.m_occupied)), m_first_available(v.m_first_available) { v.m_begin = v.m_end = nullptr; v.m_capacity = 0; v.m_occupied.clear(); v.m_first_available = invalid_index; } ~flat_handle_map() { clear(); if (m_begin) { AllocatorTraits::deallocate(m_alloc, m_begin, m_capacity); } } flat_handle_map& operator=(const flat_handle_map& v) { if (this == &v) { // prevent self usurp return *this; } clear(); assign_impl(v); return *this; } flat_handle_map& operator=(flat_handle_map&& v) { clear(); m_occupied = std::move(v.m_occupied); m_first_available = v.m_first_available; m_alloc = std::move(v.m_alloc); m_capacity = v.m_capacity; m_begin = v.m_begin; m_end = v.m_end; v.m_begin = v.m_end = nullptr; v.m_capacity = 0; v.m_occupied.clear(); v.m_first_available = invalid_index; return *this; } allocator_type get_allocator() const noexcept { return m_alloc; } const_reference at(handle h) const noexcept { return *(m_begin + static_cast(h)); } reference at(handle h) noexcept { return *(m_begin + static_cast(h)); } const_reference operator[](handle h) const noexcept { return at(h); } reference operator[](handle h) noexcept { return at(h); } const_reference front() const noexcept { return at(handle{get_next_occupied(0)}); } reference front() noexcept { return at(handle{get_next_occupied(0)}); } const_reference back() const noexcept { return at(handle{get_previous_occupied(m_occupied.size() - 1)}); } reference back() noexcept { return at(handle{get_previous_occupied(m_occupied.size() - 1)}); } const_pointer data() const noexcept { return m_begin; } pointer data() noexcept { return m_begin; } // iterators iterator begin() noexcept { return iterator(*this, get_next_occupied(0)); } const_iterator begin() const noexcept { return const_iterator(*this, get_next_occupied(0)); } const_iterator cbegin() const noexcept { return const_iterator(*this, get_next_occupied(0)); } iterator end() noexcept { return iterator(*this, invalid_index); } const_iterator end() const noexcept { return const_iterator(*this, invalid_index); } const_iterator cend() const noexcept { return const_iterator(*this, invalid_index); } iterator iterate(handle h) noexcept { return iterator(*this, static_cast(h)); } const_iterator iterate(handle h) const noexcept { return const_iterator(*this, static_cast(h)); } // capacity bool empty() const noexcept { return m_begin == m_end || size() == 0; } size_t size() const noexcept { return std::count(m_occupied.begin(), m_occupied.end(), true); } size_t container_size() const noexcept { return m_end - m_begin; } void reserve(size_t desired_capacity) { if (desired_capacity <= m_capacity) { return; } m_occupied.reserve(desired_capacity); size_type new_cap = find_capacity(desired_capacity); auto new_buf = AllocatorTraits::allocate(m_alloc, new_cap); const auto s = container_size(); // now we need to transfer the existing elements into the new buffer for (size_type i = 0; i < s; ++i) { if (m_occupied[i]) AllocatorTraits::construct(m_alloc, new_buf + i, std::move(*(m_begin + i))); } if (m_begin) { // free old elements for (size_type i = 0; i < s; ++i) { if (m_occupied[i]) AllocatorTraits::destroy(m_alloc, m_begin + i); } AllocatorTraits::deallocate(m_alloc, m_begin, m_capacity); } m_begin = new_buf; m_end = new_buf + s; m_capacity = new_cap; } size_t capacity() const noexcept { return m_capacity; } // modifiers void clear() noexcept { for (size_t i = 0; i < m_occupied.size(); i++) { if (m_occupied[i]) AllocatorTraits::destroy(m_alloc, m_begin + i); } m_occupied.clear(); m_first_available = invalid_index; m_end = m_begin; } handle insert(const value_type& val) { handle result = static_cast(invalid_index); if (m_first_available >= container_size()) { result = static_cast(container_size()); auto pos = grow_end(1); AllocatorTraits::construct(m_alloc, pos, val); m_occupied.push_back(true); m_first_available = invalid_index; } else { result = static_cast(m_first_available); AllocatorTraits::construct(m_alloc, m_begin + m_first_available, val); m_occupied[m_first_available] = true; m_first_available = get_next_available(m_first_available + 1); } return result; } handle insert(value_type&& val) { handle result{invalid_index}; if (m_first_available >= container_size()) { result = static_cast(container_size()); auto pos = grow_end(1); AllocatorTraits::construct(m_alloc, pos, std::move(val)); m_occupied.push_back(true); m_first_available = invalid_index; } else { result = static_cast(m_first_available); AllocatorTraits::construct(m_alloc, m_begin + m_first_available, std::move(val)); m_occupied[m_first_available] = true; m_first_available = get_next_available(m_first_available + 1); } return result; } void insert(size_type count, const value_type& val) { reserve(size() + count); for (size_type i = 0; i < count; ++i) insert(val); } template ())> void insert(InputIterator first, InputIterator last) { reserve(size() + last - first); for (auto p = first; p != last; ++p) insert(*p); } void insert(std::initializer_list ilist) { reserve(size() + ilist.size()); for (auto& elem : ilist) insert(elem); } template handle emplace(Args&&... args) { handle result{invalid_index}; if (m_first_available >= container_size()) { result = static_cast(container_size()); auto pos = grow_end(1); AllocatorTraits::construct(m_alloc, pos, std::forward(args)...); m_occupied.push_back(true); m_first_available = invalid_index; } else { result = static_cast(m_first_available); AllocatorTraits::construct(m_alloc, m_begin + m_first_available, std::forward(args)...); m_occupied[m_first_available] = true; m_first_available = get_next_available(m_first_available + 1); } return result; } iterator erase(handle h) { auto i = static_cast(h); assert(i < m_occupied.size() && m_occupied[i]); AllocatorTraits::destroy(m_alloc, m_begin + i); m_occupied[i] = false; if (i < m_first_available) m_first_available = i; return iterator(*this, get_next_occupied(i + 1)); } iterator erase(const_iterator it) { return erase(it.get_handle()); } private: size_t get_next_available(size_t i) const noexcept { for (size_t j = i; j < m_occupied.size(); j++) if (!m_occupied[j]) return j; return invalid_index; } size_t get_next_occupied(size_t i) const noexcept { for (size_t j = i; j < m_occupied.size(); j++) if (m_occupied[j]) return j; return invalid_index; } size_t get_previous_occupied(size_t i) const noexcept { static_assert(size_t(0) < size_t(0) - 1, "size_t must underflow when decrementing from zero"); for (size_t j = i; j < m_occupied.size(); j--) if (m_occupied[j]) return j; return invalid_index; } size_type find_capacity(size_type desired_capacity) const { if (m_capacity == 0) { return desired_capacity; } else { auto new_cap = m_capacity; while (new_cap < desired_capacity) { // grow by roughly 1.5 new_cap *= 3; ++new_cap; new_cap /= 2; } return new_cap; } } T* grow_end(size_t num) { const auto s = container_size(); reserve(s + num); m_end = m_begin + s + num; return m_begin + s; } // grows buffer only on empty void grow_empty(size_t capacity) { assert(m_begin == m_end); if (capacity <= m_capacity) return; if (m_begin) { AllocatorTraits::deallocate(m_alloc, m_begin, m_capacity); } m_capacity = find_capacity(capacity); m_begin = m_end = AllocatorTraits::allocate(m_alloc, m_capacity); } void assign_impl(size_type count, const T& value) { grow_empty(count); for (size_type i = 0; i < count; ++i) { insert(value); } } template void assign_impl(InputIterator first, InputIterator last) { auto isize = last - first; grow_empty(isize); for (auto p = first; p != last; ++p) { insert(*p); } } void assign_impl(std::initializer_list ilist) { grow_empty(ilist.size()); for (auto& elem : ilist) { insert(elem); } } void assign_impl(const flat_handle_map& v) { grow_empty(v.container_size()); m_occupied = v.m_occupied; m_first_available = v.m_first_available; for (size_t i = 0; i < v.container_size(); i++) if (m_occupied[i]) AllocatorTraits::construct(m_alloc, m_begin + i, *(v.m_begin + i)); m_end = m_begin + v.container_size(); } template friend bool operator==(const flat_handle_map& a, const flat_handle_map& b); pointer m_begin; pointer m_end; size_t m_capacity; Alloc m_alloc; std::vector m_occupied; size_t m_first_available = invalid_index; static constexpr size_t invalid_index = size_t(-1); }; template bool operator==(const flat_handle_map& a, const flat_handle_map& b) { using handle = typename flat_handle_map::handle; if (a.size() != b.size()) { return false; } for (size_t i = 0; i < a.m_occupied.size(); ++i) { auto h = static_cast(i); if (a.m_occupied[i] != b.m_occupied[i]) return false; if (a.m_occupied[i] && (a[h] != b[h])) return false; } return true; } template bool operator!=(const flat_handle_map& a, const flat_handle_map& b) { return !(a == b); } #ifdef HANDLEMAP_TEST void test_stable_vector() { flat_handle_map m; flat_handle_map m_copy1 = m; assert(m == m_copy1); auto h_hello = m.insert("hello"); auto h_carl = m.insert("carl"); auto h_wood = m.insert("wood"); auto h_space = m.insert(" "); m.insert({"red", "green", "blue", "alpha", "none"}); auto h_long = m.insert( "A REAAAALLY long string.........................................................:::::::::::::::::::::::...................................."s); auto h_world = m.insert("world"); m.erase(h_long); m.insert("short"); m.insert({"s", "p", "a", "m"}); string s = "s"; m.insert(s); m.emplace("p"); m.insert("a"); m.emplace("m"); for (auto it = m.iterate(h_carl); it != m.end(); ++it) { if (*it == "blue") m.erase(it); } m.erase(h_carl); auto h_jane = m.insert("jane"); auto h_black = m.insert("black"); m.erase(h_wood); auto ss = MakeString(); for (auto it = m.begin(); it != m.end(); ++it) { ss << *it << ", "; } string str = (string)ss; auto str2 = m[h_hello] + m[h_space] + m[h_world]; assert(str == "hello, jane, , red, green, black, alpha, none, short, world, s, p, a, m, s, p, a, m, "); assert(str2 == "hello world"); flat_handle_map m_copy2{m}; assert(m == m_copy2); assert(m_copy1 != m_copy2); } #endif