diff --git a/README.md b/README.md index 40bfd88..232da14 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # SIV (Stable Index Vector) -A header-only C++ library providing a vector container with stable IDs for accessing elements, even after insertions and deletions. +A header-only C++17 library providing a vector container with stable IDs for accessing elements, even after insertions and deletions. Follows STL naming conventions and mimics `std::vector`'s interface. ## Features - **Stable IDs**: Objects are accessed via IDs that remain valid regardless of other insertions/deletions -- **Handle System**: Smart handle objects that can detect if their referenced object has been erased +- **Handle System**: Smart handle objects with generation tracking to detect use-after-erase - **Cache-Friendly**: Data stored contiguously in memory for efficient iteration +- **Custom Allocator Support**: `siv::vector` with allocator propagation, like `std::vector` +- **STL-Compatible**: Familiar `std::vector`-like interface (iterators, type aliases, `at()`, `front()`, `back()`, etc.) - **Header-Only**: Single header file, easy to integrate +- **`-fno-exceptions` Compatible**: Works with both `-fexceptions` and `-fno-exceptions` ## Installation @@ -30,47 +33,50 @@ struct Entity { }; int main() { - siv::Vector entities; - + siv::vector entities; + // Add objects - returns a stable ID - siv::ID player = entities.emplace_back(0, 0, "Player"); - siv::ID enemy = entities.emplace_back(10, 5, "Enemy"); - + siv::id_type player = entities.push_back({0, 0, "Player"}); + siv::id_type enemy = entities.push_back({10, 5, "Enemy"}); + // Access via ID entities[player].x = 5; - + + // Bounds-checked access + entities.at(player).y = 10; + // Erase objects - other IDs remain valid entities.erase(enemy); - + // player ID still works! - std::cout << entities[player].name << std::endl; + printf("%s\n", entities[player].name.c_str()); } ``` ### Using Handles -Handles are smart references that know when their object has been deleted: +Handles are smart references that detect when their object has been deleted: ```cpp -siv::Vector entities; -siv::ID id = entities.emplace_back(0, 0, "Test"); +siv::vector entities; +siv::id_type id = entities.push_back({0, 0, "Test"}); // Create a handle -siv::Handle handle = entities.createHandle(id); +siv::handle h = entities.make_handle(id); // Use like a pointer -handle->x = 10; -(*handle).y = 20; +h->x = 10; +(*h).y = 20; // Check validity -if (handle.isValid()) { +if (h.valid()) { // Safe to use } // After erasing, handle becomes invalid entities.erase(id); -if (!handle) { - std::cout << "Object was deleted!" << std::endl; +if (!h) { + printf("Object was deleted!\n"); } ``` @@ -84,49 +90,170 @@ for (auto& entity : entities) { entity.x += 1; } -// Access underlying vector -std::vector& data = entities.getData(); +// Reverse iteration +for (auto it = entities.rbegin(); it != entities.rend(); ++it) { + it->y -= 1; +} + +// Direct data pointer access +Entity* ptr = entities.data(); ``` ### Conditional Removal ```cpp -entities.remove_if([](const Entity& e) { - return e.health <= 0; +// Member function +entities.erase_if([](const Entity& e) { + return e.x < 0; +}); + +// Free function (C++20 style) - returns number of elements removed +auto removed = siv::erase_if(entities, [](const Entity& e) { + return e.x < 0; }); ``` +### Custom Allocator + +Use a custom allocator just like `std::vector`: + +```cpp +#include + +// Pool allocator, tracking allocator, etc. +template +using my_allocator = std::allocator; // your custom allocator + +siv::vector> entities; +siv::id_type id = entities.push_back({0, 0, "Test"}); + +// Handles carry the allocator type +siv::handle> h = entities.make_handle(id); + +// Or construct with an allocator instance +my_allocator alloc; +siv::vector> entities2(alloc); +``` + ## API Reference -### `siv::Vector` +### `siv::vector` + +`Allocator` defaults to `std::allocator`. + +#### Member Types + +| Type | Definition | +|------|------------| +| `value_type` | `T` | +| `allocator_type` | `Allocator` | +| `size_type` | `std::size_t` | +| `difference_type` | `std::ptrdiff_t` | +| `reference` / `const_reference` | `T&` / `const T&` | +| `pointer` / `const_pointer` | `T*` / `const T*` | +| `iterator` / `const_iterator` | Random access iterators | +| `reverse_iterator` / `const_reverse_iterator` | Reverse iterators | + +#### Element Access | Method | Description | |--------|-------------| -| `push_back(obj)` | Copy object, returns ID | -| `emplace_back(args...)` | Construct in-place, returns ID | -| `erase(id)` | Remove object by ID | -| `operator[](id)` | Access object by ID | -| `size()` / `empty()` | Container size queries | -| `createHandle(id)` | Create a validity-tracking handle | -| `isValid(id, validity_id)` | Check if ID is still valid | +| `operator[](id)` | Access by ID (no bounds check) | +| `at(id)` | Access by ID (throws `std::out_of_range` or asserts) | +| `front()` / `back()` | First / last element in data order | +| `data()` | Pointer to underlying contiguous storage | + +#### Capacity + +| Method | Description | +|--------|-------------| +| `empty()` | Check if container is empty | +| `size()` | Number of elements | +| `max_size()` | Maximum possible number of elements | +| `capacity()` | Current allocated capacity | | `reserve(n)` | Pre-allocate memory | -| `clear()` | Remove all objects | +| `shrink_to_fit()` | Reduce memory to fit current size | +| `get_allocator()` | Returns a copy of the allocator | -### `siv::Handle` +#### Modifiers | Method | Description | |--------|-------------| -| `operator->` / `operator*` | Access underlying object | -| `isValid()` | Check if referenced object still exists | -| `getID()` | Get the associated ID | +| `push_back(value)` | Copy or move an object, returns stable ID | +| `emplace_back(args...)` | Construct in-place, returns stable ID | +| `pop_back()` | Remove last element in data order | +| `erase(id)` | Remove object by stable ID | +| `erase(handle)` | Remove object referenced by handle | +| `erase_at(idx)` | Remove object by data index | +| `erase_if(pred)` | Remove all elements matching predicate | +| `clear()` | Remove all objects, invalidate all handles | + +#### Iterators + +| Method | Description | +|--------|-------------| +| `begin()` / `end()` | Forward iterators | +| `cbegin()` / `cend()` | Const forward iterators | +| `rbegin()` / `rend()` | Reverse iterators | +| `crbegin()` / `crend()` | Const reverse iterators | + +#### Stable-ID Operations + +| Method | Description | +|--------|-------------| +| `make_handle(id)` | Create a validity-tracking handle | +| `make_handle_at(idx)` | Create a handle from a data index | +| `contains(id)` | Check if ID references a live object | +| `is_valid(id, generation)` | Check if ID + generation pair is still valid | +| `generation(id)` | Get current generation counter for an ID | +| `index_of(id)` | Get the current data index for an ID | +| `next_id()` | Peek at the next ID that would be assigned | + +### `siv::handle` + +`Allocator` defaults to `std::allocator`. Must match the allocator of the owning `siv::vector`. + +| Method | Description | +|--------|-------------| +| `operator->` / `operator*` | Access underlying object (asserts validity) | +| `valid()` | Check if referenced object still exists | +| `id()` | Get the associated stable ID | +| `generation()` | Get the generation at handle creation time | | `operator bool()` | Implicit validity check | +### Non-member Functions + +| Function | Description | +|----------|-------------| +| `siv::erase_if(vec, pred)` | Remove matching elements, return count removed | +| `operator==`, `!=`, `<`, `<=`, `>`, `>=` | Lexicographic comparison of elements | + +### Constants + +| Name | Description | +|------|-------------| +| `siv::id_type` | Alias for `uint64_t` | +| `siv::invalid_id` | Sentinel value (`std::numeric_limits::max()`) | + ## How It Works - Objects are stored contiguously in a data vector - An index vector maps stable IDs to current data positions -- On deletion, the last element is swapped into the gap -- Validity IDs detect use-after-erase scenarios +- On deletion, the last element is swapped into the gap (O(1) erase) +- Generation counters detect use-after-erase scenarios +- Deleted ID slots are recycled on the next insertion + +## Safety & Design Guarantees + +- **Double-erase protection**: `erase(id)` asserts that the ID is valid and the object has not already been erased +- **Handle validity tracking**: Handles use generation counters to detect use-after-erase; dereferencing an invalid handle triggers an assertion +- **No dangling handle pointers**: `siv::vector` is non-copyable and non-movable, preventing handles from pointing to a destroyed container +- **Bounds-checked access**: `at(id)` throws `std::out_of_range` (or asserts with `-fno-exceptions`); `generation()` and `index_of()` assert on invalid IDs +- **`[[nodiscard]]` on insertions**: `push_back` and `emplace_back` return values cannot be silently discarded +- **Exception safety**: Basic guarantee with self-recovery. Internal metadata uses reserve-before-modify to prevent desync on allocation failure. If element construction throws, the recycled slot is reclaimed on the next insertion +- **Allocator propagation**: Custom allocators are properly rebound for internal metadata and index vectors via `std::allocator_traits::rebind_alloc` +- **Comparison semantics**: Comparison operators operate on data-order (internal storage order), which may differ from insertion order after deletions +- **Thread safety**: Same guarantees as `std::vector` — concurrent reads are safe, concurrent writes require external synchronization ## Requirements @@ -135,4 +262,4 @@ entities.remove_if([](const Entity& e) { ## License -[Add your license here] +MIT License - see [LICENSE](LICENSE) file. diff --git a/index_vector.hpp b/index_vector.hpp index 6fe55ff..52bf49d 100644 --- a/index_vector.hpp +++ b/index_vector.hpp @@ -1,432 +1,508 @@ #pragma once + +#include +#include +#include #include +#include +#include #include -#include namespace siv { - /** Alias the differentiate between IDs and index. - * An ID allows to access the data through the index vector and is associated with the same object until its erased. - * An index is simply the current position of the object in the data vector and may change with deletions. - */ - using ID = uint64_t; + /// Stable identifier type. Maps to an object through the index indirection layer. + using id_type = uint64_t; - static constexpr ID InvalidID = std::numeric_limits::max(); + inline constexpr id_type invalid_id = std::numeric_limits::max(); - /// Forward declaration - template - class Vector; + template> + class vector; - /** A standalone struct allowing to access an object without the need to have a reference to the containing Vector. + /** A standalone smart reference to an object managed by a siv::vector. + * Tracks validity via a generation counter to detect use-after-erase. * - * @tparam TObjectType The type of the object + * @tparam T The type of the referenced object + * @tparam Allocator The allocator type used by the owning vector */ - /** Standalone object to access an object - * - * @tparam TObjectType The object's type - */ - template - class Handle + template> + class handle { public: - /// Default constructor - Handle() = default; - /// Constructor - Handle(ID id, ID validity_id, Vector* vector) - : m_id{id} - , m_validity_id{validity_id} - , m_vector{vector} - {} + handle() = default; - /// Pointer-like access to the underlying object - TObjectType* operator->() + T* operator->() { + assert(valid() && "Dereferencing invalid handle"); return &(*m_vector)[m_id]; } - /// Const pointer-like access to the object - TObjectType const* operator->() const + const T* operator->() const { + assert(valid() && "Dereferencing invalid handle"); return &(*m_vector)[m_id]; } - /// Dereference operator - TObjectType& operator*() + T& operator*() { + assert(valid() && "Dereferencing invalid handle"); return (*m_vector)[m_id]; } - /// Dereference constant operator - TObjectType const& operator*() const + const T& operator*() const { + assert(valid() && "Dereferencing invalid handle"); return (*m_vector)[m_id]; } - /// Returns the ID of the associated object [[nodiscard]] - ID getID() const + id_type id() const noexcept { return m_id; } - /** Bool operator to test against the validity of the reference - * - * @return false if uninitialized or if the object has been erased from the vector, true otherwise - */ - explicit operator bool() const + [[nodiscard]] + id_type generation() const noexcept { - return isValid(); + return m_generation; + } + + explicit operator bool() const noexcept + { + return valid(); } - /** Check if the reference is associated with a vector and has a correct validity ID - * - * @return false if uninitialized or if the object has been erased from the vector, true otherwise - */ [[nodiscard]] - bool isValid() const + bool valid() const noexcept { - return m_vector && m_vector->isValid(m_id, m_validity_id); + return m_vector && m_vector->is_valid(m_id, m_generation); } private: - /// The ID of the object. - ID m_id = 0; - /// The validity ID of the object at the time of creation. Used to check the validity of the handle. - ID m_validity_id = 0; - /// A raw pointer to the vector containing the object associated with this handle - Vector* m_vector = nullptr; - - /// Used to perform debug checks - friend class Vector; + handle(id_type id, id_type generation, vector* vec) + : m_id{id} + , m_generation{generation} + , m_vector{vec} + {} + + id_type m_id = 0; + id_type m_generation = 0; + vector* m_vector = nullptr; + + friend class vector; }; - /** A vector that provide stable IDs when adding objects. - * These IDs will still allow to access their associated objects even after inserting of removing other objects. - * This comes at the cost of a small overhead because of an additional indirection. + /** A vector providing stable IDs for element access. + * IDs remain valid across insertions and deletions of other elements. + * Data is stored contiguously for cache-friendly iteration. * - * @tparam TObjectType The type of the objects to be stored in the vector. It has to be movable. + * @tparam T The element type. Must be move-constructible and move-assignable. + * @tparam Allocator The allocator type. Defaults to std::allocator. */ - template - class Vector + template + class vector { + struct metadata + { + id_type rid = 0; + id_type generation = 0; + }; + + using metadata_allocator_type = typename std::allocator_traits::template rebind_alloc; + using index_allocator_type = typename std::allocator_traits::template rebind_alloc; + public: - Vector() = default; + // -- Member types (std::vector compatible) -- + + using value_type = T; + using allocator_type = Allocator; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = typename std::vector::iterator; + using const_iterator = typename std::vector::const_iterator; + using reverse_iterator = typename std::vector::reverse_iterator; + using const_reverse_iterator = typename std::vector::const_reverse_iterator; + + // -- Constructors / assignment -- + + vector() = default; + + explicit vector(const Allocator& alloc) + : m_data(alloc) + , m_metadata(metadata_allocator_type(alloc)) + , m_indexes(index_allocator_type(alloc)) + {} - /** Copies the provided object at the end of the vector - * - * @param object The object to copy - * @return The ID to retrieve the object + /// Non-copyable and non-movable to prevent dangling handle pointers + vector(const vector&) = delete; + vector& operator=(const vector&) = delete; + vector(vector&&) = delete; + vector& operator=(vector&&) = delete; + + // -- Element access -- + + /** Bounds-checked access by ID. + * @throws std::out_of_range if exceptions are enabled, otherwise asserts */ - ID push_back(const TObjectType& object) + reference at(id_type id) { - const ID id = getFreeSlot(); - m_data.push_back(object); - return id; + check_at(id); + return m_data[m_indexes[id]]; } - /** Constructs an object in place - * - * @tparam TArgs Constructor arguments types - * @param args Constructor arguments - * @return The ID to retrieve the object - */ - template - ID emplace_back(TArgs&&... args) + const_reference at(id_type id) const { - const ID id = getFreeSlot(); - m_data.emplace_back(std::forward(args)...); - return id; + check_at(id); + return m_data[m_indexes[id]]; } - /** Removes the object from the vector - * - * @param id The ID of the object to remove - */ - void erase(ID id) - { - // Fetch relevant info - const ID data_id = m_indexes[id]; - const ID last_data_id = m_data.size() - 1; - const ID last_id = m_metadata[last_data_id].rid; - // Update validity ID - ++m_metadata[data_id].validity_id; - // Swap the object to delete with the object at the end - std::swap(m_data[data_id], m_data[last_data_id]); - std::swap(m_metadata[data_id], m_metadata[last_data_id]); - std::swap(m_indexes[id], m_indexes[last_id]); - // Destroy the object - m_data.pop_back(); + /// Access element by stable ID (no bounds checking) + reference operator[](id_type id) + { + return m_data[m_indexes[id]]; } - /** Removes the object from the vector - * - * @param idx The index in the data vector of the object to remove - */ - void eraseViaData(uint32_t idx) + const_reference operator[](id_type id) const { - erase(m_metadata[idx].rid); + return m_data[m_indexes[id]]; } - /** Removes the object referenced by the handle from the vector - * - * @param handle The handle referencing the object to remove - */ - void erase(const Handle& handle) + reference front() { - // Ensure the handle is from this vector - assert(handle.m_vector == this); - // Ensure the object hasn't already been erased - assert(handle.isValid()); - erase(handle.getID()); + return m_data.front(); } - /** Return the index in the data vector of the object referenced by the provided ID - * - * @param id The ID to find the data index of - * @return The index in the data vector associated with the ID - */ - [[nodiscard]] - uint64_t getDataIndex(ID id) const + const_reference front() const { - return m_indexes[id]; + return m_data.front(); } - /** Access the object reference by the provided ID - * - * @param id The object's ID - * @return A reference to the object - */ - TObjectType& operator[](ID id) + reference back() { - return m_data[m_indexes[id]]; + return m_data.back(); } - /** Access the object reference by the provided ID - * - * @param id The object's ID - * @return A constant reference to the object - */ - TObjectType const& operator[](ID id) const + const_reference back() const { - return m_data[m_indexes[id]]; + return m_data.back(); } - /// Returns the number of objects in the vector - [[nodiscard]] - size_t size() const + pointer data() noexcept { - return m_data.size(); + return m_data.data(); } - /// Tells if the vector is currently empty - [[nodiscard]] - bool empty() const + const_pointer data() const noexcept + { + return m_data.data(); + } + + // -- Iterators -- + + iterator begin() noexcept { return m_data.begin(); } + iterator end() noexcept { return m_data.end(); } + const_iterator begin() const noexcept { return m_data.begin(); } + const_iterator end() const noexcept { return m_data.end(); } + const_iterator cbegin() const noexcept { return m_data.cbegin(); } + const_iterator cend() const noexcept { return m_data.cend(); } + + reverse_iterator rbegin() noexcept { return m_data.rbegin(); } + reverse_iterator rend() noexcept { return m_data.rend(); } + const_reverse_iterator rbegin() const noexcept { return m_data.rbegin(); } + const_reverse_iterator rend() const noexcept { return m_data.rend(); } + const_reverse_iterator crbegin() const noexcept { return m_data.crbegin(); } + const_reverse_iterator crend() const noexcept { return m_data.crend(); } + + // -- Capacity -- + + [[nodiscard]] bool empty() const noexcept { return m_data.empty(); } + [[nodiscard]] size_type size() const noexcept { return m_data.size(); } + [[nodiscard]] size_type max_size() const noexcept { return m_data.max_size(); } + [[nodiscard]] size_type capacity() const noexcept { return m_data.capacity(); } + + void reserve(size_type new_cap) { - return m_data.empty(); + m_data.reserve(new_cap); + m_metadata.reserve(new_cap); + m_indexes.reserve(new_cap); } - /// Returns the vector's capacity (i.e. the number of allocated slots in the vector) + /// Shrinks the data vector. Index/metadata vectors are not shrunk (needed for ID recycling). + void shrink_to_fit() + { + m_data.shrink_to_fit(); + } + + /// Returns a copy of the allocator [[nodiscard]] - size_t capacity() const + allocator_type get_allocator() const noexcept { - return m_data.capacity(); + return m_data.get_allocator(); } - /** Creates a handle pointing to the provided ID - * - * @param id The ID of the object - * @return A handle to the object - */ - Handle createHandle(ID id) + // -- Modifiers -- + + /// Removes all elements and invalidates all existing handles + void clear() { - /* Ensure the object is valid. If the data index is greater than the current size - * it means that it has been swapped and removed. */ - assert(getDataIndex(id) < size()); - return {id, m_metadata[m_indexes[id]].validity_id, this}; + m_data.clear(); + for (auto& m : m_metadata) { + ++m.generation; + } } - /** Creates a handle to an object using its position in the data vector - * - * @param idx The index of the object in the data vector - * @return A handle to the object + /** Copies the provided object at the end of the vector + * @return The stable ID to retrieve the object */ - Handle createHandleFromData(uint64_t idx) + [[nodiscard]] + id_type push_back(const T& value) { - /* Ensure the object is valid. If the data index is greater than the current size - * it means that it has been swapped and removed. */ - assert(idx < size()); - return {m_metadata[idx].rid, m_metadata[idx].validity_id, this}; + const id_type id = get_free_slot(); + m_data.push_back(value); + return id; } - /** Checks if the provided object is still valid considering its last known validity ID - * - * @param id The ID of the object - * @param validity_id The last known validity ID - * @return True if the last known validity ID is equal to the current one + /** Moves the provided object at the end of the vector + * @return The stable ID to retrieve the object */ [[nodiscard]] - bool isValid(ID id, ID validity_id) const + id_type push_back(T&& value) { - return validity_id == m_metadata[m_indexes[id]].validity_id; + const id_type id = get_free_slot(); + m_data.push_back(std::move(value)); + return id; } - /// Begin iterator of the data vector - typename std::vector::iterator begin() noexcept + /** Constructs an element in-place at the end of the vector + * @return The stable ID to retrieve the object + */ + template + [[nodiscard]] + id_type emplace_back(Args&&... args) { - return m_data.begin(); + const id_type id = get_free_slot(); + m_data.emplace_back(std::forward(args)...); + return id; } - /// End iterator of the data vector - typename std::vector::iterator end() noexcept + /// Removes the last element in data order + void pop_back() { - return m_data.end(); + assert(!empty() && "pop_back on empty vector"); + erase_at(m_data.size() - 1); + } + + /** Removes the object referenced by the provided stable ID + * @param id The stable ID of the object to remove + */ + void erase(id_type id) + { + assert(id < m_indexes.size() && "ID out of range"); + assert(m_indexes[id] < m_data.size() && "Object already erased or ID invalid"); + const id_type data_idx = m_indexes[id]; + const id_type last_data_idx = m_data.size() - 1; + const id_type last_id = m_metadata[last_data_idx].rid; + ++m_metadata[data_idx].generation; + std::swap(m_data[data_idx], m_data[last_data_idx]); + std::swap(m_metadata[data_idx], m_metadata[last_data_idx]); + std::swap(m_indexes[id], m_indexes[last_id]); + m_data.pop_back(); } - /// Const begin iterator of the data vector - typename std::vector::const_iterator begin() const noexcept + /** Removes the object referenced by the handle + * @param h A handle to the object to remove + */ + void erase(const handle& h) { - return m_data.begin(); + assert(h.m_vector == this && "Handle does not belong to this vector"); + assert(h.valid() && "Handle references an erased object"); + erase(h.id()); } - /// Const end iterator of the data vector - typename std::vector::const_iterator end() const noexcept + /** Removes the object at the given data index + * @param idx Position in the contiguous data array + */ + void erase_at(size_type idx) { - return m_data.end(); + assert(idx < m_data.size() && "Index out of range"); + erase(m_metadata[idx].rid); } - /** Removes all objects that match the provided predicate - * - * @tparam TCallback The callback's type, any callable should be fine - * @param callback The predicate used to check an object has to be removed + /** Removes all elements matching the predicate (C++20-style member) + * @param predicate Unary predicate returning true for elements to remove */ - template - void remove_if(TCallback&& predicate) + template + void erase_if(Pred&& predicate) { - for (uint32_t i{0}; i < m_data.size();) { + for (size_type i{0}; i < m_data.size();) { if (predicate(m_data[i])) { - eraseViaData(i); + erase_at(i); } else { ++i; } } } - /** Pre allocates @p size slots in the vector - * - * @param size The number of slots to allocate in the vector + // -- Stable-ID specific operations -- + + /** Returns the current data index for the given ID + * @param id The stable ID */ - void reserve(size_t size) + [[nodiscard]] + size_type index_of(id_type id) const { - m_data.reserve(size); - m_metadata.reserve(size); - m_indexes.reserve(size); + assert(id < m_indexes.size() && "ID out of range"); + return m_indexes[id]; } - /// Return the validity ID associated with the provided ID - [[nodiscard]] - ID getValidityID(ID id) const + /** Creates a handle pointing to the given stable ID + * @param id The stable ID of a live object + */ + handle make_handle(id_type id) { - return m_metadata[m_indexes[id]].validity_id; + assert(id < m_indexes.size() && m_indexes[id] < m_data.size()); + return {id, m_metadata[m_indexes[id]].generation, this}; } - /// Returns a raw pointer to the first element of the data vector - TObjectType* data() + /** Creates a handle from a data index + * @param idx Position in the contiguous data array + */ + handle make_handle_at(size_type idx) { - return m_data.data(); + assert(idx < size()); + return {m_metadata[idx].rid, m_metadata[idx].generation, this}; } - /// Returns a reference to the data vector - std::vector& getData() + /** Checks if an ID + generation pair still references a live object. + * Used internally by handle::valid(). + */ + [[nodiscard]] + bool is_valid(id_type id, id_type generation) const noexcept { - return m_data; + if (id >= m_indexes.size() || m_indexes[id] >= m_metadata.size()) { + return false; + } + return generation == m_metadata[m_indexes[id]].generation; } - /// Returns a constant reference to the data vector - const std::vector& getData() const + /// Returns the generation counter for the given ID + [[nodiscard]] + id_type generation(id_type id) const { - return m_data; + assert(id < m_indexes.size() && "ID out of range"); + return m_metadata[m_indexes[id]].generation; } - /// Returns the ID that would be used if an object was added + /// Returns the ID that would be assigned to the next inserted element [[nodiscard]] - ID getNextID() const + id_type next_id() const { - // This means that we have available slots if (m_metadata.size() > m_data.size()) { return m_metadata[m_data.size()].rid; } return m_data.size(); } - /// Erase all objects and invalidates all slots - void clear() - { - // Remove all data - m_data.clear(); - - for (auto& m : m_metadata) { - // Invalidate all slots - ++m.validity_id; - } - } - + /// Checks whether the ID references a currently live object [[nodiscard]] - bool isValidID(siv::ID id) const + bool contains(id_type id) const noexcept { - return id < m_indexes.size(); + return id < m_indexes.size() && m_indexes[id] < m_data.size(); } private: - /** Creates a new slot in the vector - * - * @note If a slot is available it will be reused, if not a new one will be created. - * - * @return The ID of the newly created slot. - */ - ID getFreeSlot() + void check_at(id_type id) const + { + if (id >= m_indexes.size() || m_indexes[id] >= m_data.size()) { +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) + throw std::out_of_range("siv::vector::at: invalid id"); +#else + assert(false && "siv::vector::at: invalid id"); +#endif + } + } + + id_type get_free_slot() { - const ID id = getFreeID(); + const id_type id = get_free_id(); m_indexes[id] = m_data.size(); return id; } - /** Gets a ID to a free slot. - * - * @note If an ID is available it will be reused, if not a new one will be created. - * - * @return An ID of a free slot. - */ - ID getFreeID() + id_type get_free_id() { - // This means that we have available slots if (m_metadata.size() > m_data.size()) { - // Update the validity ID - ++m_metadata[m_data.size()].validity_id; + ++m_metadata[m_data.size()].generation; return m_metadata[m_data.size()].rid; } - // A new slot has to be created - const ID new_id = m_data.size(); + const id_type new_id = m_data.size(); + // Reserve both before modifying either to prevent desync on allocation failure + m_indexes.reserve(m_indexes.size() + 1); + m_metadata.reserve(m_metadata.size() + 1); + // After successful reserves, push_back on trivial types cannot throw m_metadata.push_back({new_id, 0}); m_indexes.push_back(new_id); return new_id; } - private: - /// The struct holding additional information about an object - struct Metadata - { - /// The reverse ID, allowing to retrieve the ID of the object from the data vector. - ID rid = 0; - /// An identifier that is changed when the object is erased, used to ensure a handle is still valid. - ID validity_id = 0; - }; - - /// The vector holding the actual objects. - std::vector m_data; - /// The vector holding the associated metadata. It is accessed using the same index as for the data vector. - std::vector m_metadata; - /// The vector that stores the data index for each ID. - std::vector m_indexes; + std::vector m_data; + std::vector m_metadata; + std::vector m_indexes; }; + + // -- Non-member functions -- + + /// Erases all elements matching the predicate (C++20-style free function) + /// @return The number of elements removed + template + typename vector::size_type erase_if(vector& v, Pred predicate) + { + const auto old_size = v.size(); + v.erase_if(std::move(predicate)); + return old_size - v.size(); + } + + /// @note Comparisons operate on elements in data-order (internal storage order), + /// which may differ from insertion order after deletions (swap-to-back). + template + bool operator==(const vector& lhs, const vector& rhs) + { + return lhs.size() == rhs.size() + && std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } + + template + bool operator!=(const vector& lhs, const vector& rhs) + { + return !(lhs == rhs); + } + + template + bool operator<(const vector& lhs, const vector& rhs) + { + return std::lexicographical_compare(lhs.begin(), lhs.end(), + rhs.begin(), rhs.end()); + } + + template + bool operator<=(const vector& lhs, const vector& rhs) + { + return !(rhs < lhs); + } + + template + bool operator>(const vector& lhs, const vector& rhs) + { + return rhs < lhs; + } + + template + bool operator>=(const vector& lhs, const vector& rhs) + { + return !(lhs < rhs); + } }