Skip to content

Commit 28fd083

Browse files
committed
Add SlotMap container
1 parent 3d0efc1 commit 28fd083

10 files changed

Lines changed: 506 additions & 0 deletions

File tree

Code/max/Containers/SlotMap.hpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2021, The max Contributors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef MAX_CONTAINERS_SLOTMAP_HPP
6+
#define MAX_CONTAINERS_SLOTMAP_HPP
7+
8+
#include <algorithm>
9+
#include <concepts>
10+
#include <utility>
11+
#include <vector>
12+
13+
#include <max/Compiling/CurrentVersionNamespace.hpp>
14+
#include <max/Compiling/AliasingOptimizations.hpp>
15+
16+
17+
namespace max
18+
{
19+
MAX_CURRENT_VERSION_NAMESPACE_BEGIN(v0)
20+
{
21+
namespace Containers
22+
{
23+
24+
// SlotMap is a container that provides a stable handle to an element that was inserted.
25+
//
26+
// Insertion: O(1)*
27+
// Access: O(1)
28+
// Removal: O(1)
29+
// *Unless the BackingType requires reallocation. Then it is O(1) amortized.
30+
template<typename T, std::integral HandleType = size_t, template <typename T2> typename BackingType = std::vector>
31+
class SlotMap {
32+
public:
33+
34+
35+
36+
// Required to satisfy the C++ "Container" requirements
37+
typedef T value_type;
38+
typedef T& reference;
39+
typedef const T& const_reference;
40+
typedef BackingType<T>::iterator iterator;
41+
typedef BackingType<T>::const_iterator const_iterator;
42+
typedef BackingType<T>::difference_type difference_type;
43+
typedef BackingType<T>::size_type size_type;
44+
45+
46+
47+
HandleType push_back(T element) noexcept;
48+
49+
template<class ...Args>
50+
HandleType emplace_back(Args&&... args) noexcept;
51+
52+
typename reference operator[](HandleType handle) noexcept;
53+
typename const_reference operator[](HandleType handle) const noexcept;
54+
55+
void remove(HandleType handle) noexcept;
56+
void pop_back() noexcept;
57+
58+
59+
60+
// Required to satisfy the C++ "Container" requirements
61+
typename iterator begin() noexcept;
62+
typename const_iterator begin() const noexcept;
63+
typename const_iterator cbegin() const noexcept;
64+
typename iterator end() noexcept;
65+
typename const_iterator end() const noexcept;
66+
typename const_iterator cend() noexcept;
67+
68+
typename size_type size() const noexcept;
69+
constexpr typename size_type max_size() const noexcept;
70+
71+
72+
73+
private:
74+
75+
// The indices are stable and act as handles.
76+
typename BackingType<HandleType> indices_;
77+
// If we wanted to prevent a user accidently reusing a handle from an object they removed,
78+
// |indices_| could be a tuple of HandleType and generation counter.
79+
// Increment the generation every time an element is removed and compare against it when accessing.
80+
// However, this is designed for performance and will assume the programmer did not make a mistake.
81+
82+
// |data_| and |reverse_indices_| are parallel. The nth element in one corresponds to the nth element in the other.
83+
// This means once we have an element's index into |data_|, we can use that same index into |reverse_indices_| to find
84+
// the element in |indices_| that points here.
85+
typename BackingType<T> data_;
86+
typename BackingType<HandleType> reverse_indices_;
87+
88+
void remove_by_index(size_t index) noexcept;
89+
90+
friend bool operator ==(const SlotMap<T, HandleType, BackingType>& lhs, const SlotMap<T, HandleType, BackingType>& rhs) noexcept;
91+
friend bool operator !=(const SlotMap<T, HandleType, BackingType>& lhs, const SlotMap<T, HandleType, BackingType>& rhs) noexcept;
92+
friend void swap(SlotMap<T, HandleType, BackingType>& lhs, SlotMap<T, HandleType, BackingType>& rhs) noexcept;
93+
94+
};
95+
96+
} // namespace Containers
97+
} // MAX_CURRENT_VERSION_NAMESPACE_BEGIN( v0 )
98+
MAX_CURRENT_VERSION_NAMESPACE_END(v0)
99+
} // namespace max
100+
101+
#include "SlotMap.inl"
102+
103+
#endif // #ifndef MAX_CONTAINERS_SLOTMAP_HPP

Code/max/Containers/SlotMap.inl

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2021, The max Contributors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include <algorithm>
6+
#include <concepts>
7+
#include <utility>
8+
#include <vector>
9+
10+
namespace max
11+
{
12+
namespace v0
13+
{
14+
namespace Containers
15+
{
16+
17+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
18+
HandleType SlotMap<T, HandleType, BackingType>::push_back(T element) noexcept {
19+
const auto data_size = data_.size();
20+
data_.push_back(std::move(element));
21+
22+
const auto reverse_indices_size = reverse_indices_.size();
23+
if (data_size == reverse_indices_size) {
24+
// No elements had previously been removed, leaving gaps.
25+
// This means we cannot reuse the handles and indices we left in place.
26+
// We must add new ones.
27+
auto new_handle = reverse_indices_size;
28+
indices_.push_back(new_handle);
29+
reverse_indices_.push_back(new_handle);
30+
return new_handle;
31+
}
32+
else {
33+
// We previously left the elements in |indices_| and |reverse_indices_|.
34+
// That means we can just reuse those handles.
35+
return reverse_indices_[data_size];
36+
}
37+
}
38+
39+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
40+
template<class ...Args>
41+
HandleType SlotMap<T, HandleType, BackingType>::emplace_back(Args&&... args) noexcept {
42+
// TODO: constructing the element then moving it to push_back() really defeats the purpose of an emplace_back.
43+
// Do this correctly.
44+
return push_back(T{ std::forward<Args>(args)... });
45+
}
46+
47+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
48+
typename SlotMap<T, HandleType, BackingType>::reference SlotMap<T, HandleType, BackingType>::operator[](HandleType handle) noexcept {
49+
// Assumes |handle| is within range.
50+
51+
const auto index = indices_[handle];
52+
return data_[index];
53+
}
54+
55+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
56+
typename SlotMap<T, HandleType, BackingType>::const_reference SlotMap<T, HandleType, BackingType>::operator[](HandleType handle) const noexcept {
57+
// Assumes |handle| is within range.
58+
59+
const auto index = indices_[handle];
60+
return data_[index];
61+
}
62+
63+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
64+
void SlotMap<T, HandleType, BackingType>::remove(HandleType handle) noexcept {
65+
remove_by_index(indices_[handle]);
66+
}
67+
68+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
69+
void SlotMap<T, HandleType, BackingType>::pop_back() noexcept {
70+
remove_by_index(data_.size() - 1);
71+
}
72+
73+
74+
75+
// Required to satisfy the C++ "Container" requirements
76+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
77+
typename SlotMap<T, HandleType, BackingType>::iterator SlotMap<T, HandleType, BackingType>::begin() noexcept {
78+
return data_.begin();
79+
}
80+
81+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
82+
typename SlotMap<T, HandleType, BackingType>::const_iterator SlotMap<T, HandleType, BackingType>::begin() const noexcept {
83+
return data_.begin();
84+
}
85+
86+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
87+
typename SlotMap<T, HandleType, BackingType>::const_iterator SlotMap<T, HandleType, BackingType>::cbegin() const noexcept {
88+
return data_.begin();
89+
}
90+
91+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
92+
typename SlotMap<T, HandleType, BackingType>::iterator SlotMap<T, HandleType, BackingType>::end() noexcept {
93+
return data_.end();
94+
}
95+
96+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
97+
typename SlotMap<T, HandleType, BackingType>::const_iterator SlotMap<T, HandleType, BackingType>::end() const noexcept {
98+
return data_.end();
99+
}
100+
101+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
102+
typename SlotMap<T, HandleType, BackingType>::const_iterator SlotMap<T, HandleType, BackingType>::cend() noexcept {
103+
return data_.end();
104+
}
105+
106+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
107+
typename SlotMap<T, HandleType, BackingType>::size_type SlotMap<T, HandleType, BackingType>::size() const noexcept {
108+
return data_.size();
109+
}
110+
111+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
112+
constexpr typename SlotMap<T, HandleType, BackingType>::size_type SlotMap<T, HandleType, BackingType>::max_size() const noexcept {
113+
return data_.max_size();
114+
}
115+
116+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
117+
void SlotMap<T, HandleType, BackingType>::remove_by_index(size_t index) noexcept {
118+
// Removing an element in the middle of a vector will cause all elements after it to shift over.
119+
// Removing the final element does not have this effect.
120+
121+
// In order to remove any given element, we first swap it with the element in the final spot.
122+
// Then we can remove it and update indices.
123+
124+
using std::swap;
125+
126+
auto data_size = data_.size() - 1; // .size() tells us the index past the end.
127+
128+
swap(data_[index], data_[data_size]);
129+
swap(reverse_indices_[index], reverse_indices_[data_size]); // We only resize |data_|, so use it's size--not |reverse_indices_|'s.
130+
data_.resize(data_size);
131+
132+
// The catch is we only know one index, the parameter.
133+
// We don't know which element in |indices_| corresponds to the final element in |data_|.
134+
// To solve that problem, the |reverse_indices_| vector is parallel to the |data_| vector.
135+
// An index into one is also an index into the other.
136+
// We have the index of the final element in |data_|, so we can use it in |reverse_indices_| to get the corresponding entry in |indices_|.
137+
auto reverse_index = reverse_indices_[data_size];
138+
indices_[reverse_index] = data_size;
139+
140+
// We also update the reverse index of the that was removed.
141+
// This is because it will be reused later when more elements are appended.
142+
reverse_index = reverse_indices_[index];
143+
indices_[reverse_index] = index;
144+
}
145+
146+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
147+
bool operator ==(const SlotMap<T, HandleType, BackingType>& lhs, const SlotMap<T, HandleType, BackingType>& rhs) noexcept {
148+
return lhs.indices_ == rhs.indices_ &&
149+
lhs.data_ == rhs.data_ &&
150+
lhs.reverse_indices_ == rhs.reverse_indices_;
151+
}
152+
153+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
154+
bool operator !=(const SlotMap<T, HandleType, BackingType>& lhs, const SlotMap<T, HandleType, BackingType>& rhs) noexcept {
155+
return !(lhs == rhs);
156+
}
157+
158+
template<typename T, std::integral HandleType, template <typename T2> typename BackingType>
159+
void swap(SlotMap<T, HandleType, BackingType>& lhs, SlotMap<T, HandleType, BackingType>& rhs) noexcept {
160+
using std::swap;
161+
162+
swap(lhs.indices_, rhs.indices_);
163+
swap(lhs.data_, rhs.data_);
164+
swap(lhs.reverse_indices_, rhs.reverse_indices_);
165+
}
166+
167+
} // namespace Containers
168+
} // namespace v0
169+
} // namespace max

0 commit comments

Comments
 (0)