userver: userver/utils/slot_map.hpp Source File
Loading...
Searching...
No Matches
slot_map.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/slot_map.hpp
4/// @brief @copybrief utils::SlotMap
5
6#include <concepts>
7#include <cstddef>
8#include <deque>
9#include <limits>
10#include <ranges>
11#include <utility>
12#include <variant>
13
14#include <userver/compiler/impl/lifetime.hpp>
15#include <userver/utils/assert.hpp>
16#include <userver/utils/fast_scope_guard.hpp>
17#include <userver/utils/impl/intrusive_link_mode.hpp>
18#include <userver/utils/meta.hpp>
19
20USERVER_NAMESPACE_BEGIN
21
22namespace utils {
23
24namespace impl::slot_map {
25
26template <typename Range>
27concept HasCapacity = requires(Range range) {
28 {
29 range.capacity()
30 } -> std::integral;
31};
32
33template <typename Range>
34concept Indexable = requires(Range range, std::size_t index) {
35 {
36 range[index]
37 } -> std::convertible_to<std::ranges::range_reference_t<Range&>>;
38};
39
40inline constexpr std::size_t kFreeListEnd = std::numeric_limits<std::size_t>::max();
41
42struct FreeNode final {
43 constexpr explicit FreeNode(std::size_t next_index) noexcept
44 : next_index(next_index)
45 {}
46
47 std::size_t next_index{kFreeListEnd};
48};
49
50} // namespace impl::slot_map
51
52/// @ingroup userver_containers
53///
54/// @brief A minimalistic slot-map container adaptor.
55///
56/// Each slot holds either a live value of type `T` or a free-list node.
57/// Erased slots are recycled so that `emplace` reuses them before allocating new storage.
58///
59/// Guarantees:
60///
61/// - `operator[](std::size_t)` is O(1);
62/// - `emplace` and `insert` are amortized O(1) (assuming that `Container` provides this guarantee);
63/// - `erase` is O(1);
64/// - `size()` and `empty()` are O(1);
65/// - iterators returned by `AliveItems()` ARE invalidated by `emplace`, `insert`,
66/// `insert_range`, and `erase`, regardless of the underlying `Container`.
67///
68/// If `Container` is `std::deque` or another reference-stable container, then inserted elements have reference
69/// stability; otherwise elements may move around on `emplace` and `insert*`, and `T` should be movable.
70///
71/// `Container` template is instantiated with an internal value type; the resulting type must:
72///
73/// - be a `std::ranges::forward_range` and `std::ranges::sized_range`;
74/// - support `operator[](std::size_t)` with computational complexity O(1);
75/// - support `emplace_back`, forwarding arbitrary `Args&&...` to the value;
76/// - support `std::ranges::size` with computational complexity O(1).
77///
78/// For example, `std::deque`, `std::vector`, `boost::small_vector` satisfy those requirements.
79///
80/// @note Not thread-safe.
81template <typename T, template <typename...> typename Container = std::deque>
82class SlotMap final {
83 using Slot = std::variant<T, impl::slot_map::FreeNode>;
84
85 static_assert(std::ranges::forward_range<Container<Slot>&>);
86 static_assert(std::ranges::forward_range<const Container<Slot>&>);
87 static_assert(std::ranges::sized_range<const Container<Slot>&>);
88 static_assert(impl::slot_map::Indexable<Container<Slot>&>);
89 static_assert(impl::slot_map::Indexable<const Container<Slot>&>);
90
91public:
92 /// @brief Result of an `emplace` or `insert` call.
94 T& element; ///< Reference to the newly inserted element.
95 std::size_t index; ///< Stable index of the newly inserted element.
96 };
97
98 /// @brief Constructs an empty SlotMap.
99 SlotMap() = default;
100
101 /// @brief Constructs a SlotMap from the elements in `[first, last)`.
102 template <std::input_iterator It, std::sentinel_for<It> Sentinel>
103 SlotMap(It first, Sentinel last) {
104 insert_range(std::ranges::subrange(std::move(first), std::move(last)));
105 }
106
107 SlotMap(const SlotMap&) = default;
108 SlotMap& operator=(const SlotMap&) = default;
109
110 SlotMap(SlotMap&&) noexcept = default;
111 SlotMap& operator=(SlotMap&&) noexcept = default;
112
113 ~SlotMap() = default;
114
115 /// @brief Constructs a new element in-place and returns a reference to it and its stable index.
116 ///
117 /// If there are free (erased) slots, one is reused; otherwise new storage is allocated.
118 ///
119 /// @note Invalidates all iterators returned by `AliveItems()`.
120 /// Does not invalidate references to existing elements if the underlying container supports it, e.g. `std::deque`.
121 template <typename... Args>
122 InsertionResult emplace(Args&&... args) USERVER_IMPL_LIFETIME_BOUND {
123 if (const auto entry = TryPopFromFreeList(); entry.slot != nullptr) {
124 try {
125 T& element = entry.slot->template emplace<T>(std::forward<Args>(args)...);
126 return {element, entry.index};
127 } catch (...) {
128 EraseAndPushToFreeList(entry);
129 throw;
130 }
131 }
132
133 const std::size_t index = std::ranges::size(slots_);
134 Slot& slot = slots_.emplace_back(std::in_place_type<T>, std::forward<Args>(args)...);
135 auto* const value = std::get_if<T>(&slot);
136 UASSERT(value != nullptr);
137 return {*value, index};
138 }
139
140 /// @brief Inserts a copy of @a value and returns a reference to it and its stable index.
141 ///
142 /// @note Invalidates all iterators returned by `AliveItems()`.
143 /// Does not invalidate references to existing elements if the underlying container supports it, e.g. `std::deque`.
144 InsertionResult insert(const T& value) USERVER_IMPL_LIFETIME_BOUND { return emplace(value); }
145
146 /// @brief Inserts @a value by move and returns a reference to it and its stable index.
147 ///
148 /// @note Invalidates all iterators returned by `AliveItems()`.
149 /// Does not invalidate references to existing elements if the underlying container supports it, e.g. `std::deque`.
150 InsertionResult insert(T&& value) USERVER_IMPL_LIFETIME_BOUND { return emplace(std::move(value)); }
151
152 /// @brief Inserts all elements from @a range.
153 ///
154 /// Provides a basic exception guarantee: if an element's constructor throws,
155 /// all previously inserted elements remain valid and the map is left in a
156 /// consistent state.
157 ///
158 /// @note Invalidates all iterators returned by `AliveItems()`.
159 /// Does not invalidate references to existing elements if the underlying container supports it, e.g. `std::deque`.
160 template <std::ranges::input_range Range>
161 void insert_range(Range&& range) {
162 for (auto&& elem : std::forward<Range>(range)) {
163 emplace(std::forward<decltype(elem)>(elem));
164 }
165 }
166
167 /// @brief Returns the number of live elements.
168 [[nodiscard]] std::size_t size() const noexcept { return std::ranges::size(slots_) - free_list_size_; }
169
170 /// @brief Returns true if there are no live elements.
171 [[nodiscard]] bool empty() const noexcept { return size() == 0; }
172
173 /// @brief Returns the total number of allocated slots (live + free).
174 ///
175 /// This is the size of the backing storage. It never shrinks.
176 [[nodiscard]] std::size_t capacity() const noexcept {
177 if constexpr (impl::slot_map::HasCapacity<const Container<Slot>&>) {
178 return slots_.capacity();
179 } else {
180 return std::ranges::size(slots_);
181 }
182 }
183
184 /// @brief Calls `reserve` on the underlying container.
185 void reserve(std::size_t capacity)
186 requires meta::IsReservable<Container<Slot>>
187 {
188 slots_.reserve(capacity);
189 }
190
191 /// @brief Returns a reference to the live element at @a index.
192 ///
193 /// Precondition: the slot at @a index holds a live element (was not erased).
194 T& operator[](std::size_t index) noexcept USERVER_IMPL_LIFETIME_BOUND {
195 UASSERT(index < std::ranges::size(slots_));
196 auto* const value = std::get_if<T>(&slots_[index]);
197 UASSERT(value != nullptr);
198 return *value;
199 }
200
201 /// @overload
202 const T& operator[](std::size_t index) const noexcept USERVER_IMPL_LIFETIME_BOUND {
203 UASSERT(index < std::ranges::size(slots_));
204 const auto* const value = std::get_if<T>(&slots_[index]);
205 UASSERT(value != nullptr);
206 return *value;
207 }
208
209 /// @brief Destroys the element at @a index and recycles the slot.
210 ///
211 /// If the slot at @a index is already free (was previously erased), this is a no-op.
212 ///
213 /// @returns the number of erased elements: `1` if a live element was erased,
214 /// `0` if the slot was already free.
215 ///
216 /// @note Invalidates all iterators returned by `AliveItems()`.
217 /// Does NOT invalidate references to other live elements.
218 std::size_t erase(std::size_t index) {
219 UASSERT(index < std::ranges::size(slots_));
220 auto& slot = slots_[index];
221 if (std::get_if<T>(&slot) == nullptr) {
222 return 0;
223 }
224 EraseAndPushToFreeList({.slot = &slot, .index = index});
225 return 1;
226 }
227
228 /// @brief Returns a view over live (non-erased) elements, yielding `T&` references.
229 ///
230 /// @warning Iterators of the returned view are invalidated by `emplace`, `insert`,
231 /// `insert_range`, and `erase`. References to elements remain valid.
232 [[nodiscard]] std::ranges::forward_range auto AliveItems() noexcept USERVER_IMPL_LIFETIME_BOUND {
233 return std::ranges::ref_view(slots_) | std::views::filter(IsLive{}) | std::views::transform(ToValue{});
234 }
235
236 /// @overload
237 [[nodiscard]] std::ranges::forward_range auto AliveItems() const noexcept USERVER_IMPL_LIFETIME_BOUND {
238 return std::ranges::ref_view(slots_) | std::views::filter(IsLive{}) | std::views::transform(ToConstValue{});
239 }
240
241private:
242 struct IsLive {
243 bool operator()(const Slot& slot) const noexcept { return std::holds_alternative<T>(slot); }
244 };
245
246 struct ToValue {
247 T& operator()(Slot& slot) const noexcept {
248 auto* const value = std::get_if<T>(&slot);
249 UASSERT(value != nullptr);
250 return *value;
251 }
252 };
253
254 struct ToConstValue {
255 const T& operator()(const Slot& slot) const noexcept {
256 const auto* const value = std::get_if<T>(&slot);
257 UASSERT(value != nullptr);
258 return *value;
259 }
260 };
261
262 struct FreeListEntry final {
263 Slot* slot;
264 std::size_t index;
265 };
266
267 FreeListEntry TryPopFromFreeList() noexcept {
268 const auto index = free_list_head_;
269
270 if (index == impl::slot_map::kFreeListEnd) {
271 return {.slot = nullptr, .index = impl::slot_map::kFreeListEnd};
272 }
273
274 Slot& slot = slots_[index];
275 const auto* const node = std::get_if<impl::slot_map::FreeNode>(&slot);
276 UASSERT(node != nullptr);
277 free_list_head_ = node->next_index;
278 --free_list_size_;
279 return {.slot = &slot, .index = index};
280 }
281
282 void EraseAndPushToFreeList(FreeListEntry entry) noexcept {
283 UASSERT(entry.slot != nullptr);
284 UASSERT(entry.slot == &slots_[entry.index]);
285 entry.slot->template emplace<impl::slot_map::FreeNode>(free_list_head_);
286 free_list_head_ = entry.index;
287 ++free_list_size_;
288 }
289
290 Container<Slot> slots_;
291 std::size_t free_list_head_{impl::slot_map::kFreeListEnd};
292 std::size_t free_list_size_{0};
293};
294
295} // namespace utils
296
297USERVER_NAMESPACE_END