Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
sparse_set.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
4
5#include <algorithm>
6#include <concepts>
7#include <cstddef>
8#include <limits>
9#include <memory>
10#include <memory_resource>
11#include <span>
12#include <string_view>
13#include <type_traits>
14#include <vector>
15
16namespace helios::container {
17
18/**
19 * @brief A sparse set data structure for efficient mapping of sparse indices to
20 * dense storage of values.
21 * @details SparseSet provides O(1) insertion, deletion, and lookup operations
22 * using two arrays:
23 * - sparse: maps element indices to dense indices
24 * - dense: stores packed values of type T in contiguous memory
25 *
26 * The data structure is particularly useful for managing sparse collections
27 * where indices may have large gaps but you want cache-friendly iteration over
28 * existing values.
29 *
30 * Memory complexity: O(max_index + num_elements)
31 * Time complexity: O(1) for all operations (amortized for insertions)
32 *
33 * @tparam T Type of values stored in the dense array
34 * @tparam IndexType Type used for element indices (default: size_t)
35 * @tparam Allocator Allocator type for memory management (default:
36 * std::allocator<T>)
37 */
38template <typename T, typename IndexType = size_t,
39 typename Allocator = std::allocator<T>>
40class SparseSet {
41private:
42 using DenseIndexType = IndexType;
43 using SparseAllocator = typename std::allocator_traits<
44 Allocator>::template rebind_alloc<DenseIndexType>;
45 using DenseAllocator = Allocator;
46 using ReverseMapAllocator = typename std::allocator_traits<
47 Allocator>::template rebind_alloc<IndexType>;
48
49 using SparseContainerType = std::vector<DenseIndexType, SparseAllocator>;
50 using DenseContainerType = std::vector<T, DenseAllocator>;
51 using ReverseMapContainerType = std::vector<IndexType, ReverseMapAllocator>;
52
53public:
54 using value_type = T;
55 using index_type = IndexType;
56 using dense_index_type = DenseIndexType;
57 using size_type = size_t;
58 using reference = T&;
59 using const_reference = const T&;
60 using pointer = T*;
61 using const_pointer = const T*;
62 using allocator_type = Allocator;
63
64 using iterator = typename DenseContainerType::iterator;
65 using const_iterator = typename DenseContainerType::const_iterator;
66 using reverse_iterator = typename DenseContainerType::reverse_iterator;
68 typename DenseContainerType::const_reverse_iterator;
69
70 static constexpr IndexType kInvalidIndex =
71 std::numeric_limits<IndexType>::max();
72 static constexpr DenseIndexType kInvalidDenseIndex =
73 std::numeric_limits<DenseIndexType>::max();
74
75 /**
76 * @brief Default constructor.
77 * @details Creates an empty sparse set.
78 * Exception safety: No-throw guarantee if `T` and `Allocator` are nothrow
79 * default constructible.
80 */
81 constexpr SparseSet() noexcept(
82 std::is_nothrow_default_constructible_v<Allocator>) = default;
83
84 /**
85 * @brief Constructor with custom allocator.
86 * @details Creates an empty sparse set with the specified allocator.
87 * Exception safety: Basic guarantee.
88 * @param alloc The allocator to use for memory management
89 * @throws May throw if allocator copy constructor throws
90 */
91 explicit SparseSet(const Allocator& alloc) noexcept(
92 noexcept(SparseAllocator(alloc)))
93 : sparse_(SparseAllocator(alloc)),
94 dense_(alloc),
95 reverse_map_(ReverseMapAllocator(alloc)) {}
96
97 /**
98 * @brief Constructor with PMR memory resource.
99 * @details Enabled only when `Allocator` is constructible from
100 * `std::pmr::memory_resource*`.
101 * @param resource Memory resource used to construct allocator
102 */
103 explicit SparseSet(std::pmr::memory_resource* resource) noexcept(
104 std::is_nothrow_constructible_v<Allocator, std::pmr::memory_resource*>)
105 requires std::constructible_from<Allocator, std::pmr::memory_resource*>
106 : SparseSet(Allocator{resource}) {}
107
108 SparseSet(std::nullptr_t) = delete;
109
110 /**
111 * @brief Copy constructor.
112 * @details Creates a copy of another sparse set.
113 * Exception safety: Strong guarantee.
114 * @param other The sparse set to copy from
115 * @throws May throw if `T` copy constructor or memory allocation fails
116 */
117 constexpr SparseSet(const SparseSet& other) = default;
118 /**
119 * @brief Move constructor.
120 * @details Transfers ownership of resources from other sparse set.
121 * Exception safety: No-throw guarantee.
122 */
123 constexpr SparseSet(SparseSet&&) noexcept = default;
124
125 /**
126 * @brief Destructor.
127 * @details Destroys the sparse set and releases all memory.
128 */
129 constexpr ~SparseSet() = default;
130
131 /**
132 * @brief Copy assignment operator.
133 * @details Assigns the contents of another sparse set to this one.
134 * Exception safety: Strong guarantee.
135 * @param other The sparse set to copy from
136 * @return Reference to this sparse set
137 * @throws May throw if `T` copy assignment or memory allocation fails
138 */
139 constexpr SparseSet& operator=(const SparseSet& other) = default;
140
141 /**
142 * @brief Move assignment operator.
143 * @details Transfers ownership of resources from other sparse set.
144 * Exception safety: No-throw guarantee.
145 * @return Reference to this sparse set
146 */
147 constexpr SparseSet& operator=(SparseSet&&) noexcept = default;
148
149 /**
150 * @brief Clears the set, removing all elements.
151 * @details Removes all values from the set.
152 * The sparse array is reset to invalid indices but its capacity is preserved
153 * for performance. Time complexity: O(sparse_capacity). Exception safety:
154 * No-throw guarantee.
155 */
156 constexpr void Clear() noexcept;
157
158 /**
159 * @brief Inserts a value at the specified index (copy version).
160 * @details Adds the specified value to the set at the given index if it's not
161 * already present. If the index already exists, the existing value is
162 * replaced. The sparse array will be resized if necessary to accommodate the
163 * index. Time complexity: O(1) amortized (O(index) worst case if sparse array
164 * needs resizing). Exception safety: Strong guarantee.
165 * @warning Triggers assertion if index is invalid or negative
166 * @param index The index to insert at
167 * @param value The value to move insert
168 * @return The dense index where the value was stored
169 * @throws std::bad_alloc If memory allocation fails during sparse array
170 * resize or dense array growth
171 */
172 constexpr DenseIndexType Insert(IndexType index, const T& value);
173
174 /**
175 * @brief Inserts a value at the specified index (move version).
176 * @details Adds the specified value to the set at the given index if it's not
177 * already present. If the index already exists, the existing value is
178 * replaced. The sparse array will be resized if necessary to accommodate the
179 * index. Time complexity: O(1) amortized (O(index) worst case if sparse array
180 * needs resizing). Exception safety: Strong guarantee.
181 * @warning Triggers assertion if index is invalid or negative
182 * @param index The index to insert at
183 * @param value The value to copy insert
184 * @return The dense index where the value was stored
185 * @throws std::bad_alloc If memory allocation fails during sparse array
186 * resize or dense array growth
187 */
188 constexpr DenseIndexType Insert(IndexType index, T&& value);
189
190 /**
191 * @brief Constructs a value in-place at the specified index.
192 * @details Constructs a value directly in the dense array at the given index.
193 * If the index already exists, the existing value is replaced.
194 * The sparse array will be resized if necessary to accommodate the index.
195 * Time complexity: O(1) amortized (O(index) worst case if sparse array needs
196 * resizing). Exception safety: Strong guarantee.
197 * @warning Triggers assertion if index is invalid or negative.
198 * @param index The index to insert at
199 * @param args Arguments to forward to T's constructor
200 * @return The dense index where the value was stored
201 * @throws std::bad_alloc If memory allocation fails during sparse array
202 * resize or dense array growth
203 */
204 template <typename... Args>
205 constexpr DenseIndexType Emplace(IndexType index, Args&&... args);
206
207 /**
208 * @brief Removes an index from the set.
209 * @details Removes the specified index from the set using swap-and-pop
210 * technique to maintain dense packing. The last element in the dense array is
211 * moved to fill the gap left by the removed element. Time complexity: O(1).
212 * Exception safety: No-throw guarantee.
213 * @warning Triggers assertion if index is invalid, negative, or doesn't
214 * exist.
215 * @param index The index to remove
216 */
217 constexpr void Remove(IndexType index) noexcept;
218
219 /**
220 * @brief Reserves space for at least n elements in the dense array.
221 * @details Ensures that the dense array can hold at least n elements without
222 * triggering a reallocation. Does not affect the sparse array capacity. Time
223 * complexity: O(1) if no reallocation, O(Size()) if reallocation occurs.
224 * Exception safety: Strong guarantee.
225 * @warning Triggers assertion if n is negative.
226 * @param n The minimum capacity to reserve
227 * @throws std::bad_alloc If memory allocation fails
228 */
229 constexpr void Reserve(size_type n) { dense_.reserve(n); }
230
231 /**
232 * @brief Reserves space for indices up to max_index in the sparse array.
233 * @details Ensures that the sparse array can accommodate indices up to
234 * max_index without triggering a reallocation. Time complexity: O(1) if no
235 * reallocation, O(max_index) if reallocation occurs. Exception safety: Strong
236 * guarantee.
237 * @warning Triggers assertion if max_index is invalid or negative.
238 * @param max_index The maximum index to accommodate
239 * @throws std::bad_alloc If memory allocation fails
240 */
241 constexpr void ReserveSparse(IndexType max_index);
242
243 /**
244 * @brief Shrinks the capacity of both arrays to fit their current size.
245 * @details Reduces memory usage by shrinking both the dense and sparse arrays
246 * to their minimum required size. Time complexity: O(Size() + SparseSize()).
247 * Exception safety: Strong guarantee.
248 * @throws std::bad_alloc If memory allocation fails during shrinking
249 */
250 constexpr void ShrinkToFit();
251
252 /**
253 * @brief Gets the value at the specified index.
254 * @details Returns a reference to the value stored at the given index.
255 * Time complexity: O(1).
256 * Exception safety: No-throw guarantee.
257 * @warning Triggers assertion if index is invalid, negative, or doesn't
258 * exist.
259 * @param index The index to access
260 * @return Reference to the value at the specified index
261 */
262 [[nodiscard]] constexpr T& Get(IndexType index) noexcept;
263
264 /**
265 * @brief Gets the value at the specified index (const version).
266 * @details Returns a const reference to the value stored at the given index.
267 * Time complexity: O(1).
268 * Exception safety: No-throw guarantee.
269 * @warning Triggers assertion if index is invalid, negative, or doesn't
270 * exist.
271 * @param index The index to access
272 * @return Const reference to the value at the specified index
273 */
274 [[nodiscard]] constexpr const T& Get(IndexType index) const noexcept;
275
276 /**
277 * @brief Gets the value at the specified dense index.
278 * @details Returns a reference to the value stored at the given dense
279 * position. Time complexity: O(1). Exception safety: No-throw guarantee.
280 * @warning Triggers assertion if dense_index is invalid, negative, or out of
281 * bounds.
282 * @param dense_index The dense position to access
283 * @return Reference to the value at the specified dense position
284 */
285 [[nodiscard]] constexpr T& GetByDenseIndex(
286 DenseIndexType dense_index) noexcept;
287
288 /**
289 * @brief Gets the value at the specified dense index (const version).
290 * @details Returns a const reference to the value stored at the given dense
291 * position. Time complexity: O(1). Exception safety: No-throw guarantee.
292 * @warning Triggers assertion if dense_index is invalid, negative, or out of
293 * bounds.
294 * @param dense_index The dense position to access
295 * @return Const reference to the value at the specified dense position
296 */
297 [[nodiscard]] constexpr const T& GetByDenseIndex(
298 DenseIndexType dense_index) const noexcept;
299
300 /**
301 * @brief Tries to get the value at the specified index.
302 * @details Returns a pointer to the value stored at the given index, or
303 * nullptr if the index doesn't exist. Time complexity: O(1). Exception
304 * safety: No-throw guarantee.
305 * @param index The index to access
306 * @return Pointer to the value at the specified index, or nullptr if index
307 * doesn't exist
308 */
309 [[nodiscard]] constexpr T* TryGet(IndexType index) noexcept;
310
311 /**
312 * @brief Tries to get the value at the specified index (const version).
313 * @details Returns a pointer to the value stored at the given index, or
314 * nullptr if the index doesn't exist. Time complexity: O(1). Exception
315 * safety: No-throw guarantee.
316 * @param index The index to access
317 * @return Pointer to the value at the specified index, or nullptr if index
318 * doesn't exist
319 */
320 [[nodiscard]] constexpr const T* TryGet(IndexType index) const noexcept;
321
322 /**
323 * @brief Swaps the contents of this sparse set with another.
324 * @details Efficiently swaps all data between two sparse sets.
325 * Time complexity: O(1).
326 * Exception safety: No-throw guarantee.
327 * @param other The sparse set to swap with
328 */
329 constexpr void Swap(SparseSet& other) noexcept;
330
331 /**
332 * @brief Non-member swap function for `SparseSet`.
333 * @details Provides a non-member swap function to enable ADL and
334 * compatibility with standard algorithms. Time complexity: O(1). Exception
335 * safety: No-throw guarantee.
336 * @param lhs First sparse set
337 * @param rhs Second sparse set
338 */
339 friend constexpr void swap(SparseSet& lhs, SparseSet& rhs) noexcept {
340 lhs.Swap(rhs);
341 }
342
343 /**
344 * @brief Checks if two sparse sets are equal.
345 * @details Two sparse sets are equal if they contain the same index-value
346 * pairs. Time complexity: O(`Size()` + `other.Size()`). Exception safety:
347 * No-throw guarantee.
348 * @param other The sparse set to compare with
349 * @return True if both sets contain the same index-value pairs
350 */
351 [[nodiscard]] bool operator==(const SparseSet& other) const noexcept
352 requires std::equality_comparable<T>;
353
354 /**
355 * @brief Checks if the set is empty.
356 * @details Returns true if no values are stored in the set.
357 * Time complexity: O(1).
358 * Exception safety: No-throw guarantee.
359 * @return True if the set contains no elements, false otherwise
360 */
361 [[nodiscard]] constexpr bool Empty() const noexcept { return dense_.empty(); }
362
363 /**
364 * @brief Checks if an index value is valid for this sparse set.
365 * @details Returns true if the index is not the reserved invalid value.
366 * Time complexity: O(1).
367 * Exception safety: No-throw guarantee.
368 * @param index The index to validate
369 * @return True if the index is valid, false if it's the reserved invalid
370 * value
371 */
372 [[nodiscard]] static constexpr bool IsValidIndex(IndexType index) noexcept {
373 return index != kInvalidIndex;
374 }
375
376 /**
377 * @brief Checks if an index exists in the set.
378 * @details Performs a comprehensive check to ensure the index is valid and
379 * exists in the set. Time complexity: O(1). Exception safety: No-throw
380 * guarantee.
381 * @warning Triggers assertion if index is invalid or negative.
382 * @param index The index to check
383 * @return True if the index exists in the set, false otherwise
384 */
385 [[nodiscard]] constexpr bool Contains(IndexType index) const noexcept;
386
387 /**
388 * @brief Returns the allocator associated with the container.
389 * @details Gets the allocator used for the dense array.
390 * Time complexity: O(1).
391 * Exception safety: No-throw guarantee.
392 * @return Copy of the allocator
393 */
394 [[nodiscard]] constexpr allocator_type GetAllocator() const noexcept {
395 return dense_.get_allocator();
396 }
397
398 /**
399 * @brief Gets the dense index for a given index.
400 * @details Returns the position of the value in the dense array for the
401 * specified index. Time complexity: O(1). Exception safety: No-throw
402 * guarantee.
403 * @warning Triggers assertion if index is invalid, negative, or doesn't
404 * exist.
405 * @param index The index to look up
406 * @return The dense index
407 */
408 [[nodiscard]] constexpr DenseIndexType GetDenseIndex(
409 IndexType index) const noexcept;
410
411 /**
412 * @brief Returns the number of values in the set.
413 * @details Returns the count of values currently stored in the set.
414 * Time complexity: O(1).
415 * Exception safety: No-throw guarantee.
416 * @return The number of values in the set
417 */
418 [[nodiscard]] constexpr size_type Size() const noexcept {
419 return dense_.size();
420 }
421
422 /**
423 * @brief Returns the maximum possible size of the set.
424 * @details Returns the theoretical maximum number of elements the set can
425 * hold. Time complexity: O(1). Exception safety: No-throw guarantee.
426 * @return The maximum possible size
427 */
428 [[nodiscard]] constexpr size_type MaxSize() const noexcept {
429 return dense_.max_size();
430 }
431
432 /**
433 * @brief Returns the capacity of the dense array.
434 * @details Returns the number of elements that can be stored in the dense
435 * array without triggering a reallocation. Time complexity: O(1). Exception
436 * safety: No-throw guarantee.
437 * @return The capacity of the dense array
438 */
439 [[nodiscard]] constexpr size_type Capacity() const noexcept {
440 return dense_.capacity();
441 }
442
443 /**
444 * @brief Returns the capacity of the sparse array.
445 * @details Returns the maximum index that can be stored without triggering a
446 * sparse array reallocation. Time complexity: O(1). Exception safety:
447 * No-throw guarantee.
448 * @return The capacity of the sparse array
449 */
450 [[nodiscard]] constexpr size_type SparseCapacity() const noexcept {
451 return sparse_.capacity();
452 }
453
454 /**
455 * @brief Returns a writable span of the packed values.
456 * @details Provides direct access to the densely packed values in the order
457 * they were inserted (with removal gaps filled by later insertions).
458 * Time complexity: O(1).
459 * Exception safety: No-throw guarantee.
460 * @return A span containing all values in the set
461 */
462 [[nodiscard]] constexpr auto Data() noexcept -> std::span<T> {
463 return dense_;
464 }
465
466 /**
467 * @brief Returns a read-only span of the packed values.
468 * @details Provides direct access to the densely packed values in the order
469 * they were inserted (with removal gaps filled by later insertions).
470 * Time complexity: O(1).
471 * Exception safety: No-throw guarantee.
472 * @return A span containing all values in the set
473 */
474 [[nodiscard]] constexpr auto Data() const noexcept -> std::span<const T> {
475 return dense_;
476 }
477
478 [[nodiscard]] constexpr iterator begin() noexcept { return dense_.begin(); }
479 [[nodiscard]] constexpr const_iterator begin() const noexcept {
480 return dense_.begin();
481 }
482 [[nodiscard]] constexpr const_iterator cbegin() const noexcept {
483 return dense_.cbegin();
484 }
485
486 [[nodiscard]] constexpr iterator end() noexcept { return dense_.end(); }
487 [[nodiscard]] constexpr const_iterator end() const noexcept {
488 return dense_.end();
489 }
490 [[nodiscard]] constexpr const_iterator cend() const noexcept {
491 return dense_.cend();
492 }
493
494 [[nodiscard]] constexpr reverse_iterator rbegin() noexcept {
495 return dense_.rbegin();
496 }
497 [[nodiscard]] constexpr const_reverse_iterator rbegin() const noexcept {
498 return dense_.rbegin();
499 }
500 [[nodiscard]] constexpr const_reverse_iterator crbegin() const noexcept {
501 return dense_.crbegin();
502 }
503
504 [[nodiscard]] constexpr reverse_iterator rend() noexcept {
505 return dense_.rend();
506 }
507 [[nodiscard]] constexpr const_reverse_iterator rend() const noexcept {
508 return dense_.rend();
509 }
510 [[nodiscard]] constexpr const_reverse_iterator crend() const noexcept {
511 return dense_.crend();
512 }
513
514private:
515 SparseContainerType sparse_; ///< Maps index -> dense index
516 DenseContainerType dense_; ///< Packed values in insertion order
517 ReverseMapContainerType reverse_map_; ///< Maps dense index -> original index
518 ///< for efficient removal
519};
520
521template <typename T, typename IndexType, typename Allocator>
523 dense_.clear();
524 reverse_map_.clear();
525 std::ranges::fill(sparse_, kInvalidDenseIndex);
526}
527
528template <typename T, typename IndexType, typename Allocator>
529constexpr auto SparseSet<T, IndexType, Allocator>::Insert(IndexType index,
530 const T& value)
531 -> DenseIndexType {
533 "Failed to insert value: index is invalid!");
534 if constexpr (std::is_signed_v<IndexType>) {
535 HELIOS_ASSERT(index >= 0,
536 "Failed to insert value: index cannot be negative!");
537 }
538
539 // Check if index already exists and replace the value
540 if (Contains(index)) {
541 const DenseIndexType dense_index = sparse_[index];
542 dense_[dense_index] = value;
543 return dense_index;
544 }
545
546 // Ensure sparse array can accommodate this index
547 if (index >= sparse_.size()) {
548 sparse_.resize(index + 1, kInvalidDenseIndex);
549 }
550
551 const auto dense_index = static_cast<DenseIndexType>(dense_.size());
552 dense_.push_back(value);
553 reverse_map_.push_back(index);
554 sparse_[index] = dense_index;
555
556 return dense_index;
557}
558
559template <typename T, typename IndexType, typename Allocator>
560constexpr auto SparseSet<T, IndexType, Allocator>::Insert(IndexType index,
561 T&& value)
562 -> DenseIndexType {
564 "Failed to insert value: index is invalid!");
565 if constexpr (std::is_signed_v<IndexType>) {
566 HELIOS_ASSERT(index >= 0,
567 "Failed to insert value: index cannot be negative!");
568 }
569
570 // Check if index already exists and replace the value
571 if (Contains(index)) {
572 const DenseIndexType dense_index = sparse_[index];
573 dense_[dense_index] = std::move(value);
574 return dense_index;
575 }
576
577 // Ensure sparse array is large enough
578 if (static_cast<size_type>(index) >= sparse_.size()) {
579 sparse_.resize(static_cast<size_type>(index) + 1, kInvalidDenseIndex);
580 }
581
582 const auto dense_index = static_cast<DenseIndexType>(dense_.size());
583 sparse_[index] = dense_index;
584 dense_.push_back(std::move(value));
585 reverse_map_.push_back(index);
586
587 return dense_index;
588}
589
590template <typename T, typename IndexType, typename Allocator>
591template <typename... Args>
592constexpr auto SparseSet<T, IndexType, Allocator>::Emplace(IndexType index,
593 Args&&... args)
594 -> DenseIndexType {
596 "Failed to emplace value: index is invalid!");
597 if constexpr (std::is_signed_v<IndexType>) {
598 HELIOS_ASSERT(index >= 0,
599 "Failed to emplace value: index cannot be negative!");
600 }
601
602 // Check if index already exists and replace the value
603 if (Contains(index)) {
604 const DenseIndexType dense_index = sparse_[index];
605 dense_[dense_index] = T(std::forward<Args>(args)...);
606 return dense_index;
607 }
608
609 // Ensure sparse array is large enough
610 if (static_cast<size_type>(index) >= sparse_.size()) {
611 sparse_.resize(static_cast<size_type>(index) + 1, kInvalidDenseIndex);
612 }
613
614 const auto dense_index = static_cast<DenseIndexType>(dense_.size());
615 sparse_[index] = dense_index;
616 dense_.emplace_back(std::forward<Args>(args)...);
617 reverse_map_.push_back(index);
618
619 return dense_index;
620}
621
622template <typename T, typename IndexType, typename Allocator>
624 IndexType index) noexcept {
626 "Failed to remove value: index is invalid!");
627 if constexpr (std::is_signed_v<IndexType>) {
628 HELIOS_ASSERT(index >= 0,
629 "Failed to remove value: index cannot be negative!");
630 }
631 HELIOS_ASSERT(Contains(index),
632 "Failed to remove value: index does not exist!");
633
634 const DenseIndexType dense_index = sparse_[index];
635 const auto last_dense_index = static_cast<DenseIndexType>(dense_.size() - 1);
636
637 if (dense_index != last_dense_index) {
638 // Swap with last element to maintain dense packing
639 const IndexType last_index = reverse_map_[last_dense_index];
640 dense_[dense_index] = std::move(dense_[last_dense_index]);
641 reverse_map_[dense_index] = last_index;
642 sparse_[last_index] = dense_index;
643 }
644
645 dense_.pop_back();
646 reverse_map_.pop_back();
647 sparse_[index] = kInvalidDenseIndex;
648}
649
650template <typename T, typename IndexType, typename Allocator>
652 IndexType max_index) {
653 HELIOS_ASSERT(IsValidIndex(max_index),
654 "Failed to reserve sparse: max_index is invalid!");
655 if constexpr (std::is_signed_v<IndexType>) {
656 HELIOS_ASSERT(max_index >= 0,
657 "Failed to reserve sparse: max_index cannot be negative!");
658 }
659 if (static_cast<size_type>(max_index) + 1 > sparse_.size()) {
660 sparse_.resize(static_cast<size_type>(max_index) + 1, kInvalidDenseIndex);
661 }
662}
663
664template <typename T, typename IndexType, typename Allocator>
666 dense_.shrink_to_fit();
667 reverse_map_.shrink_to_fit();
668
669 // Find the maximum index in use to determine minimum sparse size needed
670 if (reverse_map_.empty()) {
671 sparse_.clear();
672 } else {
673 const IndexType max_index = *std::ranges::max_element(reverse_map_);
674 sparse_.resize(static_cast<size_type>(max_index) + 1);
675 }
676 sparse_.shrink_to_fit();
677}
678
679template <typename T, typename IndexType, typename Allocator>
680constexpr T& SparseSet<T, IndexType, Allocator>::Get(IndexType index) noexcept {
681 HELIOS_ASSERT(IsValidIndex(index), "Failed to get value: index is invalid!");
682 if constexpr (std::is_signed_v<IndexType>) {
683 HELIOS_ASSERT(index >= 0, "Failed to get value: index cannot be negative!");
684 }
685 HELIOS_ASSERT(Contains(index), "Failed to get value: index does not exist!");
686 return dense_[sparse_[index]];
687}
688
689template <typename T, typename IndexType, typename Allocator>
691 IndexType index) const noexcept {
692 HELIOS_ASSERT(IsValidIndex(index), "Failed to get value: index is invalid!");
693 if constexpr (std::is_signed_v<IndexType>) {
694 HELIOS_ASSERT(index >= 0, "Failed to get value: index cannot be negative!");
695 }
696 HELIOS_ASSERT(Contains(index), "Failed to get value: index does not exist!");
697 return dense_[sparse_[index]];
698}
699
700template <typename T, typename IndexType, typename Allocator>
702 DenseIndexType dense_index) noexcept {
703 HELIOS_ASSERT(dense_index != kInvalidDenseIndex,
704 "Failed to get value: dense_index is invalid!");
705 if constexpr (std::is_signed_v<DenseIndexType>) {
706 HELIOS_ASSERT(dense_index >= 0,
707 "Failed to get value: dense_index cannot be negative!");
708 }
709 HELIOS_ASSERT(dense_index < dense_.size(),
710 "Failed to get value: dense_index is out of bounds!");
711 return dense_[dense_index];
712}
713
714template <typename T, typename IndexType, typename Allocator>
716 DenseIndexType dense_index) const noexcept {
717 HELIOS_ASSERT(dense_index != kInvalidDenseIndex,
718 "Failed to get value: dense_index is invalid!");
719 if constexpr (std::is_signed_v<DenseIndexType>) {
720 HELIOS_ASSERT(dense_index >= 0,
721 "Failed to get value: dense_index cannot be negative!");
722 }
723 HELIOS_ASSERT(dense_index < dense_.size(),
724 "Failed to get value: dense_index is out of bounds!");
725 return dense_[dense_index];
726}
727
728template <typename T, typename IndexType, typename Allocator>
730 IndexType index) noexcept {
732 "Failed to try get value: index is invalid!");
733 if constexpr (std::is_signed_v<IndexType>) {
734 HELIOS_ASSERT(index >= 0,
735 "Failed to try get value: index cannot be negative!");
736 }
737
738 if (!Contains(index)) {
739 return nullptr;
740 }
741 return &dense_[sparse_[index]];
742}
743
744template <typename T, typename IndexType, typename Allocator>
746 IndexType index) const noexcept {
748 "Failed to get value: index is invalid!");
749 if constexpr (std::is_signed_v<IndexType>) {
750 HELIOS_ASSERT(index >= 0,
751 "Failed to try get value: index cannot be negative!");
752 }
753
754 if (!Contains(index)) {
755 return nullptr;
756 }
757 return &dense_[sparse_[index]];
758}
759
760template <typename T, typename IndexType, typename Allocator>
762 SparseSet& other) noexcept {
763 std::swap(sparse_, other.sparse_);
764 std::swap(dense_, other.dense_);
765 std::swap(reverse_map_, other.reverse_map_);
766}
767
768template <typename T, typename IndexType, typename Allocator>
770 const SparseSet& other) const noexcept
771 requires std::equality_comparable<T>
772{
773 if (Size() != other.Size()) {
774 return false;
775 }
776
777 for (size_type i = 0; i < dense_.size(); ++i) {
778 const IndexType index = reverse_map_[i];
779 if (!other.Contains(index) || dense_[i] != other.Get(index)) {
780 return false;
781 }
782 }
783
784 return true;
785}
786
787template <typename T, typename IndexType, typename Allocator>
789 IndexType index) const noexcept {
791 "Failed to check if set contains index: index is invalid!");
792 if constexpr (std::is_signed_v<IndexType>) {
794 index >= 0,
795 "Failed to check if set contains index: index cannot be negative!");
796 }
797 return static_cast<size_type>(index) < sparse_.size() &&
798 sparse_[index] != kInvalidDenseIndex &&
799 static_cast<size_type>(sparse_[index]) < dense_.size() &&
800 static_cast<size_type>(sparse_[index]) < reverse_map_.size() &&
801 reverse_map_[sparse_[index]] == index;
802}
803
804template <typename T, typename IndexType, typename Allocator>
806 IndexType index) const noexcept -> DenseIndexType {
808 "Failed to get dense index: index is invalid!");
809 if constexpr (std::is_signed_v<IndexType>) {
810 HELIOS_ASSERT(index >= 0,
811 "Failed to get dense index: index cannot be negative!");
812 }
813 HELIOS_ASSERT(Contains(index),
814 "Failed to get dense index: index does not exist!");
815 return sparse_[index];
816}
817
818template <typename T, typename IndexType = size_t>
821
822} // namespace helios::container
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
A sparse set data structure for efficient mapping of sparse indices to dense storage of values.
constexpr T & GetByDenseIndex(DenseIndexType dense_index) noexcept
Gets the value at the specified dense index.
constexpr const T & Get(IndexType index) const noexcept
Gets the value at the specified index (const version).
typename DenseContainerType::const_reverse_iterator const_reverse_iterator
constexpr const_iterator cbegin() const noexcept
constexpr const T & GetByDenseIndex(DenseIndexType dense_index) const noexcept
Gets the value at the specified dense index (const version).
constexpr SparseSet(SparseSet &&) noexcept=default
Move constructor.
constexpr SparseSet(const SparseSet &other)=default
Copy constructor.
constexpr bool Contains(IndexType index) const noexcept
Checks if an index exists in the set.
constexpr const_iterator begin() const noexcept
constexpr iterator end() noexcept
typename DenseContainerType::reverse_iterator reverse_iterator
constexpr const T * TryGet(IndexType index) const noexcept
Tries to get the value at the specified index (const version).
constexpr SparseSet() noexcept(std::is_nothrow_default_constructible_v< Allocator >)=default
Default constructor.
constexpr const_reverse_iterator crend() const noexcept
constexpr size_type MaxSize() const noexcept
Returns the maximum possible size of the set.
typename DenseContainerType::const_iterator const_iterator
constexpr const_iterator cend() const noexcept
constexpr DenseIndexType Emplace(IndexType index, Args &&... args)
typename DenseContainerType::iterator iterator
constexpr const_reverse_iterator rbegin() const noexcept
DenseIndexType dense_index_type
constexpr reverse_iterator rend() noexcept
static constexpr bool IsValidIndex(IndexType index) noexcept
Checks if an index value is valid for this sparse set.
constexpr T * TryGet(IndexType index) noexcept
Tries to get the value at the specified index.
SparseSet(std::nullptr_t)=delete
constexpr allocator_type GetAllocator() const noexcept
Returns the allocator associated with the container.
constexpr T & Get(IndexType index) noexcept
Gets the value at the specified index.
constexpr void ReserveSparse(IndexType max_index)
Reserves space for indices up to max_index in the sparse array.
constexpr bool Empty() const noexcept
Checks if the set is empty.
constexpr const_iterator end() const noexcept
constexpr DenseIndexType Insert(IndexType index, const T &value)
constexpr const_reverse_iterator rend() const noexcept
constexpr const_reverse_iterator crbegin() const noexcept
constexpr auto Data() const noexcept -> std::span< const T >
Returns a read-only span of the packed values.
constexpr size_type Capacity() const noexcept
Returns the capacity of the dense array.
friend constexpr void swap(SparseSet &lhs, SparseSet &rhs) noexcept
Non-member swap function for SparseSet.
constexpr reverse_iterator rbegin() noexcept
constexpr void ShrinkToFit()
Shrinks the capacity of both arrays to fit their current size.
constexpr auto Data() noexcept -> std::span< T >
Returns a writable span of the packed values.
constexpr size_type SparseCapacity() const noexcept
Returns the capacity of the sparse array.
constexpr void Swap(SparseSet &other) noexcept
Swaps the contents of this sparse set with another.
SparseSet(std::pmr::memory_resource *resource) noexcept(std::is_nothrow_constructible_v< Allocator, std::pmr::memory_resource * >)
Constructor with PMR memory resource.
bool operator==(const SparseSet &other) const noexcept
Checks if two sparse sets are equal.
constexpr DenseIndexType GetDenseIndex(IndexType index) const noexcept
Gets the dense index for a given index.
constexpr size_type Size() const noexcept
Returns the number of values in the set.
constexpr iterator begin() noexcept
SparseSet< T, IndexType, std::pmr::polymorphic_allocator< T > > PmrSparseSet
STL namespace.