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