Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
growable_allocator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
8
9#include <algorithm>
10#include <concepts>
11#include <cstddef>
12#include <memory>
13#include <mutex>
14#include <shared_mutex>
15#include <type_traits>
16#include <vector>
17
18namespace helios::memory {
19
20/**
21 * @brief Growable allocator adapter that automatically expands capacity.
22 * @details Wraps another allocator and automatically creates additional allocator instances when capacity is exceeded.
23 * Manages multiple allocator instances and delegates allocations to them.
24 *
25 * When an allocation fails due to insufficient capacity,
26 * a new allocator instance is created with a configurable growth factor applied to the initial capacity.
27 *
28 * Supports deallocation by tracking which allocator owns each pointer.
29 *
30 * The GrowableAllocator itself is thread-safe, using shared_mutex for optimal concurrent access.
31 * Underlying allocators are already thread-safe.
32 *
33 * @note Thread-safe with optimized locking.
34 * Growth occurs only when an allocation fails due to capacity constraints.
35 * Each growth creates a new allocator instance with expanded capacity.
36 * Read operations (Stats, queries) use shared locks for concurrent access.
37 * Write operations (Allocate with growth, Deallocate, Reset) use exclusive locks.
38 *
39 * The wrapped allocator must be constructible with a single capacity argument.
40 * Compatible allocators: FrameAllocator, StackAllocator, FreeListAllocator.
41 * Not compatible with: PoolAllocator (requires additional construction parameters).
42 *
43 * Copy operations are conditionally available based on the underlying allocator.
44 * If a custom copyable allocator is provided,
45 * GrowableAllocator will automatically support copy construction and copy assignment.
46 *
47 * @tparam Allocator The underlying allocator type to wrap
48 */
49template <typename Allocator>
50class GrowableAllocator final {
51public:
52 static constexpr double kDefaultGrowthFactor = 2.0;
53
54 /**
55 * @brief Constructs a growable allocator with initial capacity.
56 * @warning Triggers assertion in next cases:
57 * - Initial capacity is 0.
58 * - Growth factor is less than or equal to 1.0.
59 * @param initial_capacity Initial capacity for the first allocator instance
60 * @param growth_factor Factor to multiply capacity by when growing (default: kDefaultGrowthFactor)
61 * @param max_allocators Maximum number of allocator instances to create (0 = unlimited)
62 */
63 explicit GrowableAllocator(size_t initial_capacity, double growth_factor = kDefaultGrowthFactor,
64 size_t max_allocators = 0);
65
66 /**
67 * @brief Copy constructor.
68 * @note Only available if Allocator is copy constructible.
69 * All built-in Helios allocators are non-copyable, so this will not be available
70 * for standard use cases. Provided for custom copyable allocator types.
71 */
73 requires std::is_copy_constructible_v<Allocator>;
74
75 /**
76 * @brief Move constructor.
77 * @note Only available if Allocator is move constructible.
78 */
80 requires std::is_move_constructible_v<Allocator>;
81
82 ~GrowableAllocator() noexcept = default;
83
84 /**
85 * @brief Copy assignment operator.
86 * @note Only available if Allocator is copy assignable.
87 * All built-in Helios allocators are non-copyable, so this will not be available
88 * for standard use cases. Provided for custom copyable allocator types.
89 */
90 GrowableAllocator& operator=(const GrowableAllocator& other)
91 requires std::is_copy_assignable_v<Allocator>;
92
93 /**
94 * @brief Move assignment operator.
95 * @note Only available if Allocator is move assignable.
96 */
97 GrowableAllocator& operator=(GrowableAllocator&& other) noexcept
98 requires std::is_move_assignable_v<Allocator>;
99
100 /**
101 * @brief Allocates memory with specified size and alignment.
102 * @details Attempts allocation from existing allocators. If all fail,
103 * creates a new allocator instance with expanded capacity.
104 * @warning Triggers assertion in next cases:
105 * - Alignment is not a power of 2.
106 * - Alignment is less than kMinAlignment.
107 * @param size Number of bytes to allocate
108 * @param alignment Alignment requirement (must be power of 2)
109 * @return AllocationResult with pointer and actual allocated size, or {nullptr, 0} on failure
110 */
111 [[nodiscard]] AllocationResult Allocate(size_t size, size_t alignment = kDefaultAlignment) noexcept;
112
113 /**
114 * @brief Allocates memory for a single object of type T.
115 * @details Convenience function that calculates size and alignment from the type.
116 * The returned memory is uninitialized - use placement new to construct the object.
117 * @tparam T Type to allocate memory for
118 * @return Pointer to allocated memory, or nullptr on failure
119 *
120 * @example
121 * @code
122 * GrowableAllocator<FrameAllocator> alloc(1024);
123 * int* ptr = alloc.Allocate<int>();
124 * if (ptr != nullptr) {
125 * new (ptr) int(42);
126 * }
127 * @endcode
128 */
129 template <typename T>
130 [[nodiscard]] T* Allocate() noexcept;
131
132 /**
133 * @brief Allocates memory for an array of objects of type T.
134 * @details Convenience function that calculates size and alignment from the type.
135 * The returned memory is uninitialized - use placement new to construct objects.
136 * @tparam T Type to allocate memory for
137 * @param count Number of objects to allocate space for
138 * @return Pointer to allocated memory, or nullptr on failure
139 *
140 * @example
141 * @code
142 * GrowableAllocator<FrameAllocator> alloc(1024);
143 * int* arr = alloc.Allocate<int>(10);
144 * @endcode
145 */
146 template <typename T>
147 [[nodiscard]] T* Allocate(size_t count) noexcept;
148
149 /**
150 * @brief Allocates and constructs a single object of type T.
151 * @details Convenience function that allocates memory and constructs the object in-place.
152 * @tparam T Type to allocate and construct
153 * @tparam Args Constructor argument types
154 * @param args Arguments to forward to T's constructor
155 * @return Pointer to constructed object, or nullptr on allocation failure
156 *
157 * @example
158 * @code
159 * GrowableAllocator<FrameAllocator> alloc(1024);
160 * auto* vec = alloc.AllocateAndConstruct<MyVec3>(1.0f, 2.0f, 3.0f);
161 * @endcode
162 */
163 template <typename T, typename... Args>
164 requires std::constructible_from<T, Args...>
165 [[nodiscard]] T* AllocateAndConstruct(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>);
166
167 /**
168 * @brief Allocates and default-constructs an array of objects of type T.
169 * @details Convenience function that allocates memory and default-constructs objects in-place.
170 * @tparam T Type to allocate and construct (must be default constructible)
171 * @param count Number of objects to allocate and construct
172 * @return Pointer to first constructed object, or nullptr on allocation failure
173 *
174 * @example
175 * @code
176 * GrowableAllocator<FrameAllocator> alloc(1024);
177 * auto* arr = alloc.AllocateAndConstructArray<MyType>(10);
178 * @endcode
179 */
180 template <typename T>
181 requires std::default_initializable<T>
182 [[nodiscard]] T* AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v<T>);
183
184 /**
185 * @brief Deallocates memory.
186 * @details Finds the allocator that owns the pointer and delegates deallocation.
187 * @warning Triggers assertion if pointer does not belong to any allocator instance.
188 * @param ptr Pointer to deallocate
189 * @param size Size of allocation (for allocators that require it)
190 */
191 void Deallocate(void* ptr, size_t size = 0) noexcept;
192
193 /**
194 * @brief Resets all allocator instances.
195 * @details Resets all allocators and removes all but the first one.
196 */
197 void Reset() noexcept;
198
199 /**
200 * @brief Checks if the allocator can grow.
201 * @return True if more allocator instances can be created
202 */
203 [[nodiscard]] bool CanGrow() const noexcept;
204
205 /**
206 * @brief Gets the number of allocator instances.
207 * @return Number of allocator instances
208 */
209 [[nodiscard]] size_t AllocatorCount() const noexcept;
210
211 /**
212 * @brief Gets combined statistics for all allocator instances.
213 * @return AllocatorStats with combined usage information
214 */
215 [[nodiscard]] AllocatorStats Stats() const noexcept;
216
217 /**
218 * @brief Gets the total capacity across all allocator instances.
219 * @return Total capacity in bytes
220 */
221 [[nodiscard]] size_t TotalCapacity() const noexcept;
222
223 /**
224 * @brief Gets the initial capacity.
225 * @return Initial capacity in bytes
226 */
227 [[nodiscard]] size_t InitialCapacity() const noexcept { return initial_capacity_; }
228
229 /**
230 * @brief Gets the growth factor.
231 * @return Growth factor
232 */
233 [[nodiscard]] double GrowthFactor() const noexcept { return growth_factor_; }
234
235 /**
236 * @brief Gets the maximum number of allocators.
237 * @return Maximum allocator count (0 = unlimited)
238 */
239 [[nodiscard]] size_t MaxAllocators() const noexcept { return max_allocators_; }
240
241private:
242 /**
243 * @brief Creates a new allocator instance with specified capacity.
244 * @param capacity Capacity for the new allocator
245 */
246 void CreateAllocator(size_t capacity);
247
248 /**
249 * @brief Finds which allocator owns a pointer.
250 * @param ptr Pointer to search for
251 * @return Pointer to the owning allocator or nullptr
252 */
253 [[nodiscard]] Allocator* FindOwningAllocator(const void* ptr) noexcept;
254
255 std::vector<Allocator> allocators_; ///< Vector of allocator instances
256 size_t initial_capacity_ = 0; ///< Initial capacity
257 double growth_factor_ = kDefaultGrowthFactor; ///< Growth factor for new allocators
258 size_t max_allocators_ = 0; ///< Maximum number of allocators (0 = unlimited)
259 size_t next_capacity_ = 0; ///< Next capacity to use for growth
260 mutable std::shared_mutex mutex_; ///< Shared mutex for optimized thread-safety
261};
262
263template <typename Allocator>
264inline GrowableAllocator<Allocator>::GrowableAllocator(size_t initial_capacity, double growth_factor,
265 size_t max_allocators)
266 : initial_capacity_(initial_capacity),
267 growth_factor_(growth_factor),
268 max_allocators_(max_allocators),
269 next_capacity_(initial_capacity) {
270 HELIOS_ASSERT(initial_capacity > 0,
271 "Failed to construct GrowableAllocator: initial_capacity must be greater than 0!");
272 HELIOS_ASSERT(growth_factor > 1.0,
273 "Failed to construct GrowableAllocator: growth_factor must be greater than 1.0, got '{}'!",
274 growth_factor);
275
276 // Create the first allocator
277 allocators_.reserve(max_allocators > 0 ? max_allocators : 4);
278 CreateAllocator(initial_capacity_);
279}
280
281template <typename Allocator>
283 requires std::is_copy_constructible_v<Allocator>
284 : initial_capacity_(other.initial_capacity_),
285 growth_factor_(other.growth_factor_),
286 max_allocators_(other.max_allocators_),
287 next_capacity_(other.next_capacity_) {
288 const std::shared_lock lock(other.mutex_);
289 allocators_ = other.allocators_;
290}
291
292template <typename Allocator>
294 requires std::is_move_constructible_v<Allocator>
295 : allocators_(std::move(other.allocators_)),
296 initial_capacity_(other.initial_capacity_),
297 growth_factor_(other.growth_factor_),
298 max_allocators_(other.max_allocators_),
299 next_capacity_(other.next_capacity_) {
300 other.initial_capacity_ = 0;
301 other.growth_factor_ = kDefaultGrowthFactor;
302 other.max_allocators_ = 0;
303 other.next_capacity_ = 0;
304}
305
306template <typename Allocator>
308 requires std::is_copy_assignable_v<Allocator>
309{
310 if (this == &other) [[unlikely]] {
311 return *this;
312 }
313
314 const std::scoped_lock lock(mutex_);
315 const std::shared_lock other_lock(other.mutex_);
316
317 allocators_ = other.allocators_;
318 initial_capacity_ = other.initial_capacity_;
319 growth_factor_ = other.growth_factor_;
320 max_allocators_ = other.max_allocators_;
321 next_capacity_ = other.next_capacity_;
322
323 return *this;
324}
325
326template <typename Allocator>
328 requires std::is_move_assignable_v<Allocator>
329{
330 if (this == &other) [[unlikely]] {
331 return *this;
332 }
333
334 const std::scoped_lock lock(mutex_);
335 const std::scoped_lock other_lock(other.mutex_);
336
337 allocators_ = std::move(other.allocators_);
338 initial_capacity_ = other.initial_capacity_;
339 growth_factor_ = other.growth_factor_;
340 max_allocators_ = other.max_allocators_;
341 next_capacity_ = other.next_capacity_;
342
343 other.initial_capacity_ = 0;
344 other.growth_factor_ = kDefaultGrowthFactor;
345 other.max_allocators_ = 0;
346 other.next_capacity_ = 0;
347
348 return *this;
349}
350
351template <typename Allocator>
352inline AllocationResult GrowableAllocator<Allocator>::Allocate(size_t size, size_t alignment) noexcept {
353 HELIOS_ASSERT(IsPowerOfTwo(alignment), "Failed to allocate memory: alignment must be power of 2, got '{}'!",
354 alignment);
355 HELIOS_ASSERT(alignment >= kMinAlignment, "Failed to allocate memory: alignment must be at least '{}', got '{}'!",
356 kMinAlignment, alignment);
357
358 if (size == 0) [[unlikely]] {
359 return {.ptr = nullptr, .allocated_size = 0};
360 }
361
362 // First, try to allocate from existing allocators with shared lock
363 {
364 const std::shared_lock lock(mutex_);
365
366 for (auto& allocator : allocators_) {
367 AllocationResult result{};
368 if constexpr (requires { allocator.Allocate(size, alignment); }) {
369 result = allocator.Allocate(size, alignment);
370 } else if constexpr (requires { allocator.Allocate(size); }) {
371 result = allocator.Allocate(size);
372 }
373 if (result.ptr != nullptr) {
374 return result;
375 }
376 }
377 }
378
379 // All existing allocators are full, need exclusive lock for growth
380 const std::scoped_lock lock(mutex_);
381
382 // Try again with exclusive lock (another thread might have grown while we waited)
383 for (auto& allocator : allocators_) {
384 AllocationResult result{};
385 if constexpr (requires { allocator.Allocate(size, alignment); }) {
386 result = allocator.Allocate(size, alignment);
387 } else if constexpr (requires { allocator.Allocate(size); }) {
388 result = allocator.Allocate(size);
389 }
390 if (result.ptr != nullptr) {
391 return result;
392 }
393 }
394
395 // Still need to grow, check if we can
396 if (max_allocators_ > 0 && allocators_.size() >= max_allocators_) {
397 return {.ptr = nullptr, .allocated_size = 0};
398 }
399
400 // Calculate next capacity
401 auto new_capacity = static_cast<size_t>(static_cast<double>(next_capacity_) * growth_factor_);
402
403 // Ensure new capacity is at least large enough for the requested allocation
404 if (new_capacity < size) {
405 new_capacity = size + (size / 2); // Add 50% extra space
406 }
407
408 // Create new allocator
409 CreateAllocator(new_capacity);
410 next_capacity_ = new_capacity;
411
412 // Try to allocate from the new allocator
413 auto& new_allocator = allocators_.back();
414 if constexpr (requires { new_allocator.Allocate(size, alignment); }) {
415 return new_allocator.Allocate(size, alignment);
416 } else if constexpr (requires { new_allocator.Allocate(size); }) {
417 return new_allocator.Allocate(size);
418 }
419 return {.ptr = nullptr, .allocated_size = 0};
420}
421
422template <typename Allocator>
423inline void GrowableAllocator<Allocator>::Deallocate(void* ptr, size_t size) noexcept {
424 if (ptr == nullptr) [[unlikely]] {
425 return;
426 }
427
428 // Check if allocator supports deallocation at compile time
429 if constexpr (
430 requires { std::declval<Allocator>().Deallocate(ptr, size); } ||
431 requires { std::declval<Allocator>().Deallocate(ptr); }) {
432 const std::shared_lock lock(mutex_);
433
434 auto* allocator = FindOwningAllocator(ptr);
435 HELIOS_ASSERT(allocator != nullptr, "Failed to deallocate memory: pointer does not belong to any allocator!");
436
437 // Delegate deallocation to the owning allocator
438 if constexpr (requires { allocator->Deallocate(ptr, size); }) {
439 allocator->Deallocate(ptr, size);
440 } else if constexpr (requires { allocator->Deallocate(ptr); }) {
441 allocator->Deallocate(ptr);
442 }
443 }
444 // else: No-op for allocators that don't support individual deallocation (e.g., FrameAllocator)
445}
446
447template <typename Allocator>
449 const std::scoped_lock lock(mutex_);
450
451 // Reset all allocators
452 for (auto& allocator : allocators_) {
453 allocator.Reset();
454 }
455
456 // Keep only the first allocator
457 if (!allocators_.empty()) {
458 auto first = std::move(allocators_.front());
459 allocators_.clear();
460 allocators_.push_back(std::move(first));
461 }
462
463 next_capacity_ = initial_capacity_;
464}
465
466template <typename Allocator>
467inline bool GrowableAllocator<Allocator>::CanGrow() const noexcept {
468 const std::shared_lock lock(mutex_);
469 return max_allocators_ == 0 || allocators_.size() < max_allocators_;
470}
471
472template <typename Allocator>
473inline size_t GrowableAllocator<Allocator>::AllocatorCount() const noexcept {
474 const std::shared_lock lock(mutex_);
475 return allocators_.size();
476}
477
478template <typename Allocator>
480 const std::shared_lock lock(mutex_);
481
482 AllocatorStats combined{};
483
484 for (const auto& allocator : allocators_) {
485 const auto stats = allocator.Stats();
486 combined.total_allocated += stats.total_allocated;
487 combined.total_freed += stats.total_freed;
488 combined.peak_usage = std::max(combined.peak_usage, stats.peak_usage);
489 combined.allocation_count += stats.allocation_count;
490 combined.total_allocations += stats.total_allocations;
491 combined.total_deallocations += stats.total_deallocations;
492 combined.alignment_waste += stats.alignment_waste;
493 }
494
495 return combined;
496}
497
498template <typename Allocator>
499inline size_t GrowableAllocator<Allocator>::TotalCapacity() const noexcept {
500 const std::shared_lock lock(mutex_);
501
502 size_t total = 0;
503 for (const auto& allocator : allocators_) {
504 total += allocator.Capacity();
505 }
506 return total;
507}
508
509template <typename Allocator>
510inline void GrowableAllocator<Allocator>::CreateAllocator(size_t capacity) {
511 if constexpr (requires { Allocator(capacity); }) {
512 allocators_.emplace_back(capacity);
513 } else {
514 HELIOS_ASSERT(false, "Failed to create allocator: Allocator type does not support construction with capacity!");
515 }
516}
517
518template <typename Allocator>
519inline Allocator* GrowableAllocator<Allocator>::FindOwningAllocator(const void* ptr) noexcept {
520 for (auto& allocator : allocators_) {
521 if constexpr (requires { allocator.Owns(ptr); }) {
522 if (allocator.Owns(ptr)) {
523 return &allocator;
524 }
525 }
526 }
527 return nullptr;
528}
529
530template <typename Allocator>
531template <typename T>
533 constexpr size_t size = sizeof(T);
534 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
535 auto result = Allocate(size, alignment);
536 return static_cast<T*>(result.ptr);
537}
538
539template <typename Allocator>
540template <typename T>
541inline T* GrowableAllocator<Allocator>::Allocate(size_t count) noexcept {
542 if (count == 0) [[unlikely]] {
543 return nullptr;
544 }
545 constexpr size_t alignment = std::max(alignof(T), kMinAlignment);
546 const size_t size = sizeof(T) * count;
547 auto result = Allocate(size, alignment);
548 return static_cast<T*>(result.ptr);
549}
550
551template <typename Allocator>
552template <typename T, typename... Args>
553 requires std::constructible_from<T, Args...>
554inline T* GrowableAllocator<Allocator>::AllocateAndConstruct(Args&&... args) noexcept(
555 std::is_nothrow_constructible_v<T, Args...>) {
556 T* ptr = Allocate<T>();
557 if (ptr != nullptr) [[likely]] {
558 std::construct_at(ptr, std::forward<Args>(args)...);
559 }
560 return ptr;
561}
562
563template <typename Allocator>
564template <typename T>
565 requires std::default_initializable<T>
567 std::is_nothrow_default_constructible_v<T>) {
568 T* ptr = Allocate<T>(count);
569 if (ptr != nullptr) [[likely]] {
570 for (size_t i = 0; i < count; ++i) {
571 std::construct_at(ptr + i);
572 }
573 }
574 return ptr;
575}
576
577} // namespace helios::memory
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
Growable allocator adapter that automatically expands capacity.
GrowableAllocator & operator=(const GrowableAllocator &other)
Copy assignment operator.
double GrowthFactor() const noexcept
Gets the growth factor.
size_t AllocatorCount() const noexcept
Gets the number of allocator instances.
size_t TotalCapacity() const noexcept
Gets the total capacity across all allocator instances.
GrowableAllocator(const GrowableAllocator &other)
Copy constructor.
AllocatorStats Stats() const noexcept
Gets combined statistics for all allocator instances.
static constexpr double kDefaultGrowthFactor
void Deallocate(void *ptr, size_t size=0) noexcept
Deallocates memory.
T * AllocateAndConstructArray(size_t count) noexcept(std::is_nothrow_default_constructible_v< T >)
void Reset() noexcept
Resets all allocator instances.
T * AllocateAndConstruct(Args &&... args) noexcept(std::is_nothrow_constructible_v< T, Args... >)
~GrowableAllocator() noexcept=default
GrowableAllocator(size_t initial_capacity, double growth_factor=kDefaultGrowthFactor, size_t max_allocators=0)
Constructs a growable allocator with initial capacity.
bool CanGrow() const noexcept
Checks if the allocator can grow.
GrowableAllocator(GrowableAllocator &&other) noexcept
Move constructor.
size_t MaxAllocators() const noexcept
Gets the maximum number of allocators.
size_t InitialCapacity() const noexcept
Gets the initial capacity.
Concept for basic allocator interface.
constexpr size_t kDefaultAlignment
Default alignment for allocations (cache line size for most modern CPUs).
constexpr bool IsPowerOfTwo(size_t size) noexcept
Helper function to check if a size is a power of 2.
constexpr size_t kMinAlignment
Minimum alignment for any allocation.
constexpr T * Allocate(Alloc &allocator) noexcept
STL namespace.
Result type for allocation operations.
void * ptr
Pointer to allocated memory, nullptr on failure.
Statistics for tracking allocator usage.
size_t total_allocated
Total bytes currently allocated.