Photon Editor Library 2.0.0-beta
A physically based renderer.
Loading...
Searching...
No Matches
TTrivialItemPool.h
Go to the documentation of this file.
1#pragma once
2
7
8#include <Common/assertion.h>
9#include <Common/memory.h>
10#include <Common/os.h>
11#include <Common/math_basics.h>
12#include <Common/exceptions.h>
13#include <Common/config.h>
14
15#include <cstddef>
16#include <type_traits>
17#include <limits>
18#include <utility>
19#include <memory>
20#include <vector>
21#include <numeric>
22#include <new>
23#include <algorithm>
24
25namespace ph::editor
26{
27
48template<typename Item, CHandleDispatcher Dispatcher = THandleDispatcher<TWeakHandle<Item>>>
49class TTrivialItemPool : public TItemPoolInterface<Item, typename Dispatcher::HandleType>
50{
51 // For us to omit item validity tracking--we always start item lifetime after allocation
52 static_assert(std::is_default_constructible_v<Item>,
53 "Item must be default constructible.");
54
55 // For us to grow the pool
56 static_assert(std::is_move_constructible_v<Item>,
57 "Item must be move constructible.");
58
59 // For us to omit destruction
60 static_assert(std::is_trivially_destructible_v<Item>,
61 "Item must be trivially destructible.");
62
63 // TODO: optimize item copy when we have std::is_implicit_lifetime
64
65public:
66 using HandleType = typename Dispatcher::HandleType;
67
68private:
69 using Index = typename HandleType::IndexType;
70 using Generation = typename HandleType::GenerationType;
71 using NonConstItem = std::remove_const_t<Item>;
72
73public:
75 : m_storageMemory()
76 , m_generations()
77 , m_dispatcher()
78 , m_numItems(0)
79 {}
80
82 requires std::is_copy_constructible_v<Item> &&
83 std::is_copy_constructible_v<Dispatcher>
84
85 : m_storageMemory()
86 , m_generations()
87 , m_dispatcher(other.m_dispatcher)
88 , m_numItems(other.m_numItems)
89 {
90 grow(other.capacity());
91
92 // Copy all items (they are either created by user or default-constructed)
93 for(Index i = 0; i < other.capacity(); ++i)
94 {
95 std::construct_at(
96 getItemPtrDirectly(m_storageMemory, i),
97 *getItemPtr(other.m_storageMemory, i));
98 }
99
100 // Copied after storage, since capacity is determined from `m_generations`
101 m_generations = other.m_generations;
102 }
103
106 {
107 swap(*this, other);
108 }
109
111 {
112 swap(*this, rhs);
113
114 return *this;
115 }
116
117 ~TTrivialItemPool() override = default;
118
119 Item* accessItem(const HandleType& handle) override
120 {
121 return get(handle);
122 }
123
124 const Item* viewItem(const HandleType& handle) const override
125 {
126 return get(handle);
127 }
128
134 [[nodiscard]]
135 HandleType add(Item item)
136 {
137 return createAt(dispatchOneHandle(), std::move(item));
138 }
139
143 void remove(const HandleType& handle)
144 {
145 returnOneHandle(removeAt(handle));
146 }
147
152 HandleType createAt(const HandleType& handle, Item item)
153 {
154 constexpr auto initialGeneration = HandleType::nextGeneration(HandleType::INVALID_GENERATION);
155
156 const bool isEmpty = handle.isEmpty();
157 const bool isInvalidOutOfBound = handle.getIndex() >= capacity() && handle.getGeneration() != initialGeneration;
158 const bool isStale = handle.getIndex() < capacity() && !isFresh(handle);
159 if(isEmpty || isInvalidOutOfBound || isStale)
160 {
161 throw_formatted<IllegalOperationException>(
162 "creating trivial item with bad handle {}", handle);
163 }
164
165 // Potentially create new storage space
166 const Index itemIdx = handle.getIndex();
167 if(itemIdx >= capacity())
168 {
169 if(capacity() == maxCapacity())
170 {
171 throw_formatted<OverflowException>(
172 "Storage size will exceed the maximum amount Index type can hold (max={})",
173 maxCapacity());
174 }
175
176 PH_ASSERT_LT(itemIdx, maxCapacity());
177 const Index newCapacity = std::max(nextCapacity(capacity()), itemIdx + 1);
178 grow(newCapacity);
179 }
180
181 // At this point, storage size must have been grown to cover `itemIdx`
182 PH_ASSERT_LT(itemIdx, m_generations.size());
183 PH_ASSERT(isFresh(handle));
184
185 // No need for storing the returned pointer nor using `std::launder()` on each use for most
186 // cases (same object type with exactly the same storage location), see C++ standard [basic.life]
187 // section 8 (https://timsong-cpp.github.io/cppwp/basic.life#8).
188 std::construct_at(getItemPtrDirectly(m_storageMemory, itemIdx), std::move(item));
189
190 ++m_numItems;
191 return handle;
192 }
193
200 [[nodiscard]]
202 {
203 if(!isFresh(handle))
204 {
205 throw_formatted<IllegalOperationException>(
206 "removing trivial item with stale handle {}", handle);
207 }
208
209 const Index itemIdx = handle.getIndex();
210 PH_ASSERT_LT(itemIdx, capacity());
211
212 // Calling dtor is not necessary as we are dealing with trivially destructible objects
213 static_assert(std::is_trivially_destructible_v<Item>);
214
215 // Instead, we clear it by default constructing a new instance
216 std::construct_at(getItemPtr(m_storageMemory, itemIdx));
217
218 const Generation newGeneration = HandleType::nextGeneration(handle.getGeneration());
219 m_generations[itemIdx] = newGeneration;
220 --m_numItems;
221 return HandleType(handle.getIndex(), newGeneration);
222 }
223
227 [[nodiscard]]
229 {
230 // Note: call the dispatcher without touching internal states, as this method may be called
231 // with a different policy (e.g., from a different thread, depending on the dispatcher used)
232 return m_dispatcher.dispatchOne();
233 }
234
238 void returnOneHandle(const HandleType& handle)
239 {
240 // Note: call the dispatcher without touching internal states, as this method may be called
241 // with a different policy (e.g., from a different thread, depending on the dispatcher used)
242 m_dispatcher.returnOne(handle);
243 }
244
251 Item* get(const HandleType& handle)
252 {
253 return isFresh(handle) ? getItemPtr(m_storageMemory, handle.getIndex()) : nullptr;
254 }
255
262 const Item* get(const HandleType& handle) const
263 {
264 return isFresh(handle) ? getItemPtr(m_storageMemory, handle.getIndex()) : nullptr;
265 }
266
267 Index numItems() const
268 {
269 PH_ASSERT_LE(m_numItems, capacity());
270 return m_numItems;
271 }
272
273 Index numFreeSpace() const
274 {
275 PH_ASSERT_LE(m_numItems, capacity());
276 return capacity() - m_numItems;
277 }
278
279 Index capacity() const
280 {
281 PH_ASSERT_LE(m_generations.size(), maxCapacity());
282 return static_cast<Index>(m_generations.size());
283 }
284
288 bool isEmpty() const
289 {
290 return numItems() == 0;
291 }
292
297 bool isFresh(const HandleType& handle) const
298 {
299 return handle.getIndex() < m_generations.size() &&
300 handle.getGeneration() == m_generations[handle.getIndex()];
301 }
302
308 Item& operator [] (const std::size_t index)
309 {
310 PH_ASSERT_LT(index, m_generations.size());
311 return *getItemPtr(m_storageMemory, index);
312 }
313
314 const Item& operator [] (const std::size_t index) const
315 {
316 PH_ASSERT_LT(index, m_generations.size());
317 return *getItemPtr(m_storageMemory, index);
318 }
320
321 static constexpr Index maxCapacity()
322 {
323 return std::numeric_limits<Index>::max();
324 }
325
326 friend void swap(TTrivialItemPool& first, TTrivialItemPool& second) noexcept
327 {
328 // Enable ADL
329 using std::swap;
330
331 swap(first.m_storageMemory, second.m_storageMemory);
332 swap(first.m_generations, second.m_generations);
333 swap(first.m_dispatcher, second.m_dispatcher);
334 swap(first.m_numItems, second.m_numItems);
335 }
336
337private:
338 void grow(const Index newCapacity)
339 {
340 const Index oldCapacity = capacity();
341 PH_ASSERT_GT(newCapacity, oldCapacity);
342
343 const auto requiredMemorySize = newCapacity * sizeof(NonConstItem);
344 const auto alignmentSize = std::lcm(alignof(NonConstItem), os::get_L1_cache_line_size_in_bytes());
345 const auto totalMemorySize = math::next_multiple(requiredMemorySize, alignmentSize);
346
347 // Create new item storage and move items over
348
349 TAlignedMemoryUniquePtr<NonConstItem> newStorageMemory =
350 make_aligned_memory<NonConstItem>(totalMemorySize, alignmentSize);
351 if(!newStorageMemory)
352 {
353 throw std::bad_alloc{};
354 }
355
356 // Copying/moving all items to new storage. No need (and no means) to check items from the
357 // old storage are valid--they are either created by user or default-constructed when their
358 // storage was first allocated.
359 for(Index i = 0; i < oldCapacity; ++i)
360 {
361 std::construct_at(
362 getItemPtrDirectly(newStorageMemory, i),
363 std::move(*getItemPtr(m_storageMemory, i)));
364 }
365
366 // Set newly created storage space to default values, since accessing items before their
367 // construction is explicitly stated to behave like they are default-constructed. Another reason
368 // is that `Item` may not be an implicit-lifetime class, so C++20's Implicit Object Creation
369 // cannot be relied upon (item lifetime may not begin unless placement new is used).
370 std::uninitialized_default_construct_n(
371 getItemPtrDirectly(newStorageMemory, oldCapacity),
372 newCapacity - oldCapacity);
373
374 // Extend generation records to cover new storage spaces
375 constexpr auto initialGeneration = HandleType::nextGeneration(HandleType::INVALID_GENERATION);
376 m_generations.resize(newCapacity, initialGeneration);
377
378 // Finally, get rid of the old item storage
379 m_storageMemory = std::move(newStorageMemory);
380 }
381
386 static auto getItemPtr(
387 const TAlignedMemoryUniquePtr<NonConstItem>& storage,
388 const std::size_t index)
389 -> NonConstItem*
390 {
391 // If `Item` is const qualified, laundering is required to prevent aggressive constant folding.
392 // See [basic.life] section 8.3 (https://timsong-cpp.github.io/cppwp/basic.life#8.3).
393 // vvv currently not required as we are storing `NonConstItem`
394 if constexpr(/* std::is_const_v<Item> || */ PH_STRICT_OBJECT_LIFETIME)
395 {
396 // UB if pointed-to object not within its lifetime
397 return std::launder(getItemPtrDirectly(storage, index));
398 }
399 // We do not need to launder even if `Item` contains const or reference members. See
400 // https://stackoverflow.com/questions/62642542/were-all-implementations-of-stdvector-non-portable-before-stdlaunder
401 else
402 {
403 return getItemPtrDirectly(storage, index);
404 }
405 }
406
407 static auto getItemPtrDirectly(
408 const TAlignedMemoryUniquePtr<NonConstItem>& storage,
409 const std::size_t index)
410 -> NonConstItem*
411 {
412 return storage.get() + index;
413 }
414
415 static constexpr Index nextCapacity(const Index currentCapacity)
416 {
417 // Effective growth rate k = 1.5
418 const Index oldCapacity = currentCapacity;
419 const Index additionalCapacity = oldCapacity / 2 + 1;
420 const Index newCapacity = (maxCapacity() - oldCapacity >= additionalCapacity)
421 ? oldCapacity + additionalCapacity : maxCapacity();
422 return newCapacity;
423 }
424
425private:
426 // LWG 3870 forbids `std::construct_at` to modify/create const objects. We store all items
427 // as `NonConstItem` and rely on implicit casts to const if required. This seems to be a weaker
428 // part of the standard and supporting const storage does not worth the risk of UB for now.
429 // See `getItemPtr()` for more important things to know for supporting const items.
430 TAlignedMemoryUniquePtr<NonConstItem> m_storageMemory;
431
432 std::vector<Generation> m_generations;
433 Dispatcher m_dispatcher;
434 Index m_numItems;
435};
436
437}// end namespace ph::editor
Definition TItemPoolInterface.h:10
Item pool for simple types. This item pool is designed to minimize execution time and memory footprin...
Definition TTrivialItemPool.h:50
TTrivialItemPool(TTrivialItemPool &&other) noexcept
Definition TTrivialItemPool.h:104
friend void swap(TTrivialItemPool &first, TTrivialItemPool &second) noexcept
Definition TTrivialItemPool.h:326
bool isEmpty() const
Definition TTrivialItemPool.h:288
void remove(const HandleType &handle)
Remove the item at the storage slot indicated by handle. Complexity: O(1).
Definition TTrivialItemPool.h:143
Index numItems() const
Definition TTrivialItemPool.h:267
HandleType dispatchOneHandle()
Definition TTrivialItemPool.h:228
static constexpr Index maxCapacity()
Definition TTrivialItemPool.h:321
HandleType add(Item item)
Definition TTrivialItemPool.h:135
Item & operator[](const std::size_t index)
Access item by index.
Definition TTrivialItemPool.h:308
Item * get(const HandleType &handle)
Get item by handle. Note that accessing items before construction is also allowed....
Definition TTrivialItemPool.h:251
TTrivialItemPool()
Definition TTrivialItemPool.h:74
TTrivialItemPool & operator=(TTrivialItemPool rhs) noexcept
Definition TTrivialItemPool.h:110
TTrivialItemPool(const TTrivialItemPool &other)
Definition TTrivialItemPool.h:81
void returnOneHandle(const HandleType &handle)
Definition TTrivialItemPool.h:238
Item * accessItem(const HandleType &handle) override
Definition TTrivialItemPool.h:119
HandleType removeAt(const HandleType &handle)
Remove the item at the storage slot indicated by handle. Manual handle management API....
Definition TTrivialItemPool.h:201
Index capacity() const
Definition TTrivialItemPool.h:279
HandleType createAt(const HandleType &handle, Item item)
Place item at the storage slot indicated by handle. Manual handle management API. Complexity: Amortiz...
Definition TTrivialItemPool.h:152
~TTrivialItemPool() override=default
Index numFreeSpace() const
Definition TTrivialItemPool.h:273
bool isFresh(const HandleType &handle) const
Definition TTrivialItemPool.h:297
const Item * get(const HandleType &handle) const
Get item by handle. Note that accessing items before construction is also allowed....
Definition TTrivialItemPool.h:262
typename Dispatcher::HandleType HandleType
Definition TTrivialItemPool.h:66
const Item * viewItem(const HandleType &handle) const override
Definition TTrivialItemPool.h:124
Definition ph_editor.h:10