Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
callable_buffer_array.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/container/details/callable_buffer_common.hpp>
4
5#include <concepts>
6#include <cstddef>
7#include <cstring>
8#include <functional>
9#include <memory>
10#include <memory_resource>
11#include <new>
12#include <tuple>
13#include <type_traits>
14#include <utility>
15#include <vector>
16
17namespace helios::container {
18
19/**
20 * @brief Implementation class for an inline callable array buffer with explicit
21 * allocator.
22 * @details Stores multiple callable instances of heterogeneous types in a
23 * single contiguous byte buffer, with embedded function pointers for
24 * type-erased invocation. Optimized for fast sequential iteration without
25 * virtual dispatch or per-callable heap allocations. Preserves insertion order.
26 *
27 * Use the `CallableBufferArray` alias for ergonomic usage.
28 *
29 * @tparam Allocator Allocator type for the internal byte buffer (default:
30 * `std::allocator<std::byte>`).
31 * @tparam Signatures Function signatures in the form void(Args...).
32 */
33template <typename Allocator, typename... Signatures>
34 requires((sizeof...(Signatures) > 0) &&
35 (details::VoidSignature<Signatures> && ...))
37public:
38 static constexpr size_t kNumOperations = sizeof...(Signatures);
39
40 using allocator_type = Allocator;
42 std::allocator_traits<allocator_type>::template rebind_alloc<std::byte>;
44 std::allocator_traits<allocator_type>::template rebind_alloc<size_t>;
45
46 using size_type = size_t;
47
48private:
49 using DestroyFn = void (*)(void*);
50 using RelocateFn = void (*)(void* dest, void* src);
51
52 /// @brief Function pointer type for the first signature (used for size
53 /// calculations).
54 using FirstExecuteFn = details::TupleToFunctionPtrType<
55 details::NthSignatureArgsT<0, Signatures...>>;
56
57 static_assert(((sizeof(details::TupleToFunctionPtrType<
58 details::NthSignatureArgsT<0, Signatures...>>) ==
59 sizeof(details::TupleToFunctionPtrType<
60 details::SignatureArgsT<Signatures>>)) &&
61 ...),
62 "All function pointer types must have the same size");
63
64 using BufferType = std::vector<std::byte, byte_allocator_type>;
65 using OffsetsType = std::vector<size_type, offset_allocator_type>;
66
67 /// @brief Pack of signature types for use in method validation.
68 using SignaturePack = std::tuple<Signatures...>;
69
70public:
71 constexpr CallableBufferArrayImpl() = default;
72
73 /**
74 * @brief Constructs with a custom allocator.
75 * @param alloc Allocator instance to use
76 */
77 explicit constexpr CallableBufferArrayImpl(const allocator_type& alloc)
78 : buffer_(byte_allocator_type(alloc)),
79 offsets_(offset_allocator_type(alloc)) {}
80
81 /**
82 * @brief Constructs with a PMR memory resource.
83 * @details Enabled only when `allocator_type` is constructible from
84 * `std::pmr::memory_resource*`.
85 * @param resource Memory resource used to construct allocator
86 */
87 explicit constexpr CallableBufferArrayImpl(
88 std::pmr::memory_resource* resource)
89 requires std::constructible_from<allocator_type, std::pmr::memory_resource*>
91
92 CallableBufferArrayImpl(std::nullptr_t) = delete;
93
96 : buffer_(std::move(other.buffer_)),
97 offsets_(std::move(other.offsets_)) {}
98
100
103
104 /**
105 * @brief Clears all stored callables, calling destructors as needed.
106 * @details Destroys in reverse insertion order.
107 */
108 void Clear() noexcept;
109
110 /**
111 * @brief Pushes a callable using its default `operator()` for invocation.
112 * @details For single-signature arrays, uses `operator()(Args...)`.
113 * For multi-signature arrays, uses
114 * `operator()(std::integral_constant<size_t, N>, Args...)`.
115 * @tparam T Callable type that must be invocable with the appropriate
116 * signature(s) and return void.
117 * @param callable Callable object to store
118 */
119 template <CallableBufferStorable T>
120 requires details::DefaultOpCallable<std::remove_cvref_t<T>, Signatures...>
121 void Push(T&& callable) {
122 PushImpl(std::forward<T>(callable));
123 }
124
125 /**
126 * @brief Pushes a callable with custom member function pointers.
127 * @details Allows specifying custom function names for each operation.
128 * Number of Methods must match `kNumOperations`. Each method must be
129 * invocable on `T` with the corresponding signature's arguments.
130 * @tparam Methods Member function pointers for each operation
131 * @tparam T Callable type (deduced)
132 * @param callable Callable object to store
133 *
134 * @code
135 * // Single operation
136 * array.Push<&MyCmd::Run>(MyCmd{});
137 *
138 * // Multiple operations
139 * array.Push<&MyCmd::Execute, &MyCmd::Log>(MyCmd{});
140 * @endcode
141 */
142 template <auto... Methods, CallableBufferStorable T>
143 requires details::AllMethodsValid<T, SignaturePack, Methods...> &&
144 (sizeof...(Methods) == kNumOperations)
145 void Push(T&& callable) {
146 PushImplMethods<Methods...>(std::forward<T>(callable));
147 }
148
149 /**
150 * @brief Invokes operation `N` on all stored callables in insertion order.
151 * @details Supports polymorphic argument conversion for flexible invocation.
152 * @tparam N Operation index (0 to `kNumOperations-1`)
153 * @tparam UArgs Actual argument types (must be convertible to signature args)
154 * @param args Arguments for operation `N`
155 */
156 template <size_t N = 0, typename... UArgs>
157 requires details::ArgsConvertibleTo<
158 details::NthSignatureArgsT<N, Signatures...>, UArgs...> &&
159 (N < sizeof...(Signatures))
160 void Invoke(UArgs&&... args) noexcept;
161
162 /**
163 * @brief Reserves bytes in the internal buffer.
164 * @param bytes Number of bytes to reserve
165 */
166 constexpr void ReserveBytes(size_type bytes) { buffer_.reserve(bytes); }
167
168 /**
169 * @brief Reserves space for approximately count callables.
170 * @details Estimates average callable size for reservation.
171 * @param count Approximate number of callables
172 */
173 constexpr void Reserve(size_type count);
174
175 /**
176 * @brief Swaps contents with another array.
177 * @param other Array to swap with
178 */
179 constexpr void Swap(CallableBufferArrayImpl& other) noexcept(
180 std::is_nothrow_swappable_v<BufferType> &&
181 std::is_nothrow_swappable_v<OffsetsType>);
182
185 rhs) noexcept(std::is_nothrow_swappable_v<BufferType> &&
186 std::is_nothrow_swappable_v<OffsetsType>) {
187 lhs.Swap(rhs);
188 }
189
190 /**
191 * @brief Merges another array into this one by moving its callables.
192 * @details Moves all callables from the other array into this one.
193 * The other array is cleared after the operation.
194 * @param other Array to merge from
195 */
196 constexpr void Merge(CallableBufferArrayImpl&& other);
197
198 /**
199 * @brief Checks if the array is empty.
200 * @return true if empty, false otherwise
201 */
202 [[nodiscard]] constexpr bool Empty() const noexcept {
203 return offsets_.empty();
204 }
205
206 /**
207 * @brief Gets the number of stored callables.
208 * @return Number of callables
209 */
210 [[nodiscard]] constexpr size_type Size() const noexcept {
211 return offsets_.size();
212 }
213
214 /**
215 * @brief Gets the current capacity in bytes of the internal buffer.
216 * @return Capacity in bytes
217 */
218 [[nodiscard]] constexpr size_type CapacityBytes() const noexcept {
219 return buffer_.capacity();
220 }
221
222 /**
223 * @brief Gets the allocator used by the array.
224 * @return Allocator instance
225 */
226 [[nodiscard]] constexpr allocator_type GetAllocator() const noexcept {
227 return allocator_type(buffer_.get_allocator());
228 }
229
230private:
231 /**
232 * @brief Header layout per callable entry:
233 * - kNumOperations function pointers (one per signature)
234 * - DestroyFn pointer (or `nullptr` if trivially destructible)
235 * - RelocateFn pointer (or `nullptr` if trivially copyable)
236 * - size_type data_offset (offset from header start to callable data)
237 * - [padding for alignment]
238 * - Callable data
239 */
240 static constexpr size_type BaseHeaderSize() noexcept {
241 return (kNumOperations * sizeof(FirstExecuteFn)) + sizeof(DestroyFn) +
242 sizeof(RelocateFn) + sizeof(size_type);
243 }
244
245 static constexpr size_type AlignUp(size_type offset,
246 size_type alignment) noexcept {
247 return (offset + alignment - 1) & ~(alignment - 1);
248 }
249
250 template <typename T, size_t N, typename... Args>
251 static constexpr void ExecuteDefault(Args... args, void* data) noexcept;
252
253 template <typename T, auto Method, typename... Args>
254 static constexpr void ExecuteMethod(Args... args, void* data) noexcept;
255
256 template <typename T>
257 static void DestroyCallable(void* data) noexcept {
258 std::destroy_at(static_cast<T*>(data));
259 }
260
261 template <typename T>
262 static constexpr void RelocateCallable(void* dest, void* src) noexcept;
263
264 void GrowBuffer(size_type required_capacity);
265
266 template <typename T>
267 void PushImpl(T&& callable);
268
269 template <auto... Methods, typename T>
270 void PushImplMethods(T&& callable);
271
272 template <typename T, size_t... Indices>
273 void StoreFunctionPointersDefault(size_type header_offset,
274 std::index_sequence<Indices...>) noexcept;
275
276 template <typename T, auto... Methods, size_t... Indices>
277 void StoreFunctionPointersMethods(size_type header_offset,
278 std::index_sequence<Indices...>) noexcept;
279
280 [[nodiscard]] void* GetDataPtr(size_type header_offset) const noexcept;
281
282 BufferType buffer_;
283 OffsetsType offsets_;
284};
285
286template <typename Allocator, typename... Signatures>
287 requires((sizeof...(Signatures) > 0) &&
288 (details::VoidSignature<Signatures> && ...))
289inline auto CallableBufferArrayImpl<Allocator, Signatures...>::operator=(
291 if (this == &other) [[unlikely]] {
292 return *this;
293 }
294
295 Clear();
296 buffer_ = std::move(other.buffer_);
297 offsets_ = std::move(other.offsets_);
298 return *this;
299}
300
301template <typename Allocator, typename... Signatures>
302 requires((sizeof...(Signatures) > 0) &&
303 (details::VoidSignature<Signatures> && ...))
304inline void
305CallableBufferArrayImpl<Allocator, Signatures...>::Clear() noexcept {
306 for (auto it = offsets_.rbegin(); it != offsets_.rend(); ++it) {
307 const auto header_offset = *it;
308 auto* header_base = buffer_.data() + header_offset;
309
310 auto destroy_fn = *std::launder(reinterpret_cast<DestroyFn*>(
311 header_base + (kNumOperations * sizeof(FirstExecuteFn))));
312
313 if (destroy_fn != nullptr) {
314 auto* data = GetDataPtr(header_offset);
315 destroy_fn(data);
316 }
317 }
318
319 buffer_.clear();
320 offsets_.clear();
321}
322
323template <typename Allocator, typename... Signatures>
324 requires((sizeof...(Signatures) > 0) &&
325 (details::VoidSignature<Signatures> && ...))
326template <size_t N, typename... UArgs>
327 requires details::ArgsConvertibleTo<
328 details::NthSignatureArgsT<N, Signatures...>, UArgs...> &&
329 (N < sizeof...(Signatures))
331 UArgs&&... args) noexcept {
332 using ArgsTuple = details::NthSignatureArgsT<N, Signatures...>;
333 using ExecuteFn = details::TupleToFunctionPtrType<ArgsTuple>;
334
335 for (const size_type header_offset : offsets_) {
336 auto* header_base = buffer_.data() + header_offset;
337 auto exec_fn = *std::launder(reinterpret_cast<ExecuteFn*>(
338 header_base + (N * sizeof(FirstExecuteFn))));
339 auto* data = GetDataPtr(header_offset);
340 exec_fn(std::forward<UArgs>(args)..., data);
341 }
342}
343
344template <typename Allocator, typename... Signatures>
345 requires((sizeof...(Signatures) > 0) &&
346 (details::VoidSignature<Signatures> && ...))
347constexpr void CallableBufferArrayImpl<Allocator, Signatures...>::Reserve(
348 size_type count) {
349 offsets_.reserve(count);
350 buffer_.reserve(count * (BaseHeaderSize() + 32));
351}
352
353template <typename Allocator, typename... Signatures>
354 requires((sizeof...(Signatures) > 0) &&
355 (details::VoidSignature<Signatures> && ...))
358 other) noexcept(std::is_nothrow_swappable_v<BufferType> &&
359 std::is_nothrow_swappable_v<OffsetsType>) {
360 buffer_.swap(other.buffer_);
361 offsets_.swap(other.offsets_);
362}
363
364template <typename Allocator, typename... Signatures>
365 requires((sizeof...(Signatures) > 0) &&
366 (details::VoidSignature<Signatures> && ...))
367constexpr void CallableBufferArrayImpl<Allocator, Signatures...>::Merge(
368 CallableBufferArrayImpl&& other) {
369 if (this == &other) [[unlikely]] {
370 return;
371 }
372
373 constexpr size_type fn_ptr_size = sizeof(FirstExecuteFn);
374
375 const auto aligned_boundary =
376 AlignUp(buffer_.size(), alignof(FirstExecuteFn));
377 const auto padding = aligned_boundary - buffer_.size();
378 const auto total_required = buffer_.size() + padding + other.buffer_.size();
379
380 if (total_required > buffer_.capacity()) {
381 GrowBuffer(total_required);
382 }
383
384 if (padding > 0) {
385 buffer_.resize(aligned_boundary, std::byte{0});
386 }
387
388 const auto offset_adjustment = aligned_boundary;
389 for (const auto offset : other.offsets_) {
390 offsets_.push_back(offset + offset_adjustment);
391 }
392 buffer_.insert(buffer_.end(), std::make_move_iterator(other.buffer_.begin()),
393 std::make_move_iterator(other.buffer_.end()));
394
395 for (const auto other_offset : other.offsets_) {
396 auto* old_header = other.buffer_.data() + other_offset;
397 auto* new_header = buffer_.data() + other_offset + offset_adjustment;
398
399 auto relocate_fn = *std::launder(reinterpret_cast<RelocateFn*>(
400 old_header + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn)));
401
402 if (relocate_fn != nullptr) {
403 const auto data_offset = *std::launder(reinterpret_cast<size_type*>(
404 old_header + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn) +
405 sizeof(RelocateFn)));
406 relocate_fn(new_header + data_offset, old_header + data_offset);
407 }
408 }
409
410 other.buffer_.clear();
411 other.offsets_.clear();
412}
413
414template <typename Allocator, typename... Signatures>
415 requires((sizeof...(Signatures) > 0) &&
416 (details::VoidSignature<Signatures> && ...))
417template <typename T, size_t N, typename... Args>
418constexpr void
419CallableBufferArrayImpl<Allocator, Signatures...>::ExecuteDefault(
420 Args... args, void* data) noexcept {
421 T* callable = static_cast<T*>(data);
422 if constexpr (kNumOperations == 1) {
423 std::invoke(*callable, args...);
424 } else {
425 std::invoke(*callable, std::integral_constant<size_t, N>{}, args...);
426 }
427}
428
429template <typename Allocator, typename... Signatures>
430 requires((sizeof...(Signatures) > 0) &&
431 (details::VoidSignature<Signatures> && ...))
432template <typename T, auto Method, typename... Args>
433constexpr void CallableBufferArrayImpl<Allocator, Signatures...>::ExecuteMethod(
434 Args... args, void* data) noexcept {
435 T* callable = static_cast<T*>(data);
436 if constexpr (std::invocable<decltype(Method), T&, Args...>) {
437 std::invoke(Method, callable, args...);
438 } else {
439 std::invoke(Method, args...);
440 }
441}
442
443template <typename Allocator, typename... Signatures>
444 requires((sizeof...(Signatures) > 0) &&
445 (details::VoidSignature<Signatures> && ...))
446template <typename T>
447constexpr void
448CallableBufferArrayImpl<Allocator, Signatures...>::RelocateCallable(
449 void* dest, void* src) noexcept {
450 T* typed_src = static_cast<T*>(src);
451 std::construct_at(static_cast<T*>(dest), std::move(*typed_src));
452 std::destroy_at(typed_src);
453}
454
455template <typename Allocator, typename... Signatures>
456 requires((sizeof...(Signatures) > 0) &&
457 (details::VoidSignature<Signatures> && ...))
458inline void CallableBufferArrayImpl<Allocator, Signatures...>::GrowBuffer(
459 size_type required_capacity) {
460 constexpr size_type fn_ptr_size = sizeof(FirstExecuteFn);
461
462 BufferType new_buffer(buffer_.get_allocator());
463 const auto new_cap = std::max(required_capacity, buffer_.capacity() * 2);
464 new_buffer.resize(new_cap);
465
466 if (!buffer_.empty()) {
467 std::memcpy(new_buffer.data(), buffer_.data(), buffer_.size());
468 }
469
470 for (auto header_offset : offsets_) {
471 auto* old_header = buffer_.data() + header_offset;
472 auto* new_header = new_buffer.data() + header_offset;
473
474 auto relocate_fn = *std::launder(reinterpret_cast<RelocateFn*>(
475 old_header + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn)));
476
477 if (relocate_fn != nullptr) {
478 const auto data_offset = *std::launder(reinterpret_cast<size_type*>(
479 old_header + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn) +
480 sizeof(RelocateFn)));
481 relocate_fn(new_header + data_offset, old_header + data_offset);
482 }
483 }
484
485 const auto used = buffer_.size();
486 buffer_ = std::move(new_buffer);
487 buffer_.resize(used);
488}
489
490template <typename Allocator, typename... Signatures>
491 requires((sizeof...(Signatures) > 0) &&
492 (details::VoidSignature<Signatures> && ...))
493template <typename T>
494inline void CallableBufferArrayImpl<Allocator, Signatures...>::PushImpl(
495 T&& callable) {
496 using DecayedT = std::remove_cvref_t<T>;
497
498 constexpr size_type element_size = sizeof(DecayedT);
499 constexpr size_type element_align = alignof(DecayedT);
500 constexpr size_type base_header = BaseHeaderSize();
501 constexpr size_type fn_ptr_size = sizeof(FirstExecuteFn);
502
503 const auto header_offset = AlignUp(buffer_.size(), alignof(FirstExecuteFn));
504 const auto unaligned_data_offset = header_offset + base_header;
505 const auto aligned_data_offset =
506 AlignUp(unaligned_data_offset, element_align);
507 const auto data_offset_from_header = aligned_data_offset - header_offset;
508 const auto total_entry_size = data_offset_from_header + element_size;
509 const auto required_size = header_offset + total_entry_size;
510
511 if (required_size > buffer_.capacity()) {
512 GrowBuffer(required_size);
513 }
514
515 buffer_.resize(required_size);
516
517 auto* header_base = buffer_.data() + header_offset;
518
519 StoreFunctionPointersDefault<DecayedT>(
520 header_offset, std::make_index_sequence<kNumOperations>{});
521
522 auto* destroy_fn_ptr = std::launder(reinterpret_cast<DestroyFn*>(
523 header_base + (kNumOperations * fn_ptr_size)));
524 if constexpr (std::is_trivially_destructible_v<DecayedT>) {
525 *destroy_fn_ptr = nullptr;
526 } else {
527 *destroy_fn_ptr = &DestroyCallable<DecayedT>;
528 }
529
530 auto* relocate_fn_ptr = std::launder(reinterpret_cast<RelocateFn*>(
531 header_base + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn)));
532 if constexpr (std::is_trivially_copyable_v<DecayedT>) {
533 *relocate_fn_ptr = nullptr;
534 } else {
535 *relocate_fn_ptr = &RelocateCallable<DecayedT>;
536 }
537
538 auto* data_offset_storage = std::launder(reinterpret_cast<size_type*>(
539 header_base + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn) +
540 sizeof(RelocateFn)));
541 *data_offset_storage = data_offset_from_header;
542
543 auto* data = header_base + data_offset_from_header;
544 std::construct_at(std::launder(reinterpret_cast<DecayedT*>(data)),
545 std::forward<T>(callable));
546
547 offsets_.push_back(header_offset);
548}
549
550template <typename Allocator, typename... Signatures>
551 requires((sizeof...(Signatures) > 0) &&
552 (details::VoidSignature<Signatures> && ...))
553template <auto... Methods, typename T>
554inline void CallableBufferArrayImpl<Allocator, Signatures...>::PushImplMethods(
555 T&& callable) {
556 using DecayedT = std::remove_cvref_t<T>;
557
558 constexpr size_type element_size = sizeof(DecayedT);
559 constexpr size_type element_align = alignof(DecayedT);
560 constexpr size_type base_header = BaseHeaderSize();
561 constexpr size_type fn_ptr_size = sizeof(FirstExecuteFn);
562
563 const auto header_offset = AlignUp(buffer_.size(), alignof(FirstExecuteFn));
564 const auto unaligned_data_offset = header_offset + base_header;
565 const auto aligned_data_offset =
566 AlignUp(unaligned_data_offset, element_align);
567 const auto data_offset_from_header = aligned_data_offset - header_offset;
568 const auto total_entry_size = data_offset_from_header + element_size;
569 const auto required_size = header_offset + total_entry_size;
570
571 if (required_size > buffer_.capacity()) {
572 GrowBuffer(required_size);
573 }
574
575 buffer_.resize(required_size);
576
577 auto* header_base = buffer_.data() + header_offset;
578
579 StoreFunctionPointersMethods<DecayedT, Methods...>(
580 header_offset, std::make_index_sequence<kNumOperations>{});
581
582 auto* destroy_fn_ptr = std::launder(reinterpret_cast<DestroyFn*>(
583 header_base + (kNumOperations * fn_ptr_size)));
584 if constexpr (std::is_trivially_destructible_v<DecayedT>) {
585 *destroy_fn_ptr = nullptr;
586 } else {
587 *destroy_fn_ptr = &DestroyCallable<DecayedT>;
588 }
589
590 auto* relocate_fn_ptr = std::launder(reinterpret_cast<RelocateFn*>(
591 header_base + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn)));
592 if constexpr (std::is_trivially_copyable_v<DecayedT>) {
593 *relocate_fn_ptr = nullptr;
594 } else {
595 *relocate_fn_ptr = &RelocateCallable<DecayedT>;
596 }
597
598 auto* data_offset_storage = std::launder(reinterpret_cast<size_type*>(
599 header_base + (kNumOperations * fn_ptr_size) + sizeof(DestroyFn) +
600 sizeof(RelocateFn)));
601 *data_offset_storage = data_offset_from_header;
602
603 auto* data = header_base + data_offset_from_header;
604 std::construct_at(std::launder(reinterpret_cast<DecayedT*>(data)),
605 std::forward<T>(callable));
606
607 offsets_.push_back(header_offset);
608}
609
610template <typename Allocator, typename... Signatures>
611 requires((sizeof...(Signatures) > 0) &&
612 (details::VoidSignature<Signatures> && ...))
613template <typename T, size_t... Indices>
614inline void
615CallableBufferArrayImpl<Allocator, Signatures...>::StoreFunctionPointersDefault(
616 size_type header_offset, std::index_sequence<Indices...>) noexcept {
617 auto* header_base = buffer_.data() + header_offset;
618
619 (
620 [header_base]<size_t Index>() {
621 using ArgsTuple = details::NthSignatureArgsT<Index, Signatures...>;
622 using ExecuteFn = details::TupleToFunctionPtrType<ArgsTuple>;
623
624 [header_base]<typename... Args>(std::tuple<Args...>*) {
625 auto* fn_ptr = std::launder(reinterpret_cast<ExecuteFn*>(
626 header_base + (Index * sizeof(FirstExecuteFn))));
627 *fn_ptr = &ExecuteDefault<T, Index, Args...>;
628 }(static_cast<ArgsTuple*>(nullptr));
629 }.template operator()<Indices>(),
630 ...);
631}
632
633template <typename Allocator, typename... Signatures>
634 requires((sizeof...(Signatures) > 0) &&
635 (details::VoidSignature<Signatures> && ...))
636template <typename T, auto... Methods, size_t... Indices>
637inline void
638CallableBufferArrayImpl<Allocator, Signatures...>::StoreFunctionPointersMethods(
639 size_type header_offset, std::index_sequence<Indices...>) noexcept {
640 auto* header_base = buffer_.data() + header_offset;
641
642 (
643 [header_base]<size_t Index>() {
644 using ArgsTuple = details::NthSignatureArgsT<Index, Signatures...>;
645 using ExecuteFn = details::TupleToFunctionPtrType<ArgsTuple>;
646
647 [header_base]<typename... Args>(std::tuple<Args...>*) {
648 auto* fn_ptr = std::launder(reinterpret_cast<ExecuteFn*>(
649 header_base + (Index * sizeof(FirstExecuteFn))));
650 *fn_ptr = &ExecuteMethod<T, details::kNthMethod<Index, Methods...>,
651 Args...>;
652 }(static_cast<ArgsTuple*>(nullptr));
653 }.template operator()<Indices>(),
654 ...);
655}
656
657template <typename Allocator, typename... Signatures>
658 requires((sizeof...(Signatures) > 0) &&
659 (details::VoidSignature<Signatures> && ...))
660inline void* CallableBufferArrayImpl<Allocator, Signatures...>::GetDataPtr(
661 size_type header_offset) const noexcept {
662 auto* header_base = buffer_.data() + header_offset;
663 const auto data_offset = *std::launder(reinterpret_cast<const size_type*>(
664 header_base + (kNumOperations * sizeof(FirstExecuteFn)) +
665 sizeof(DestroyFn) + sizeof(RelocateFn)));
666 return const_cast<std::byte*>(buffer_.data() + header_offset + data_offset);
667}
668
669namespace details {
670
671/// @brief Deduces the `CallableBufferArrayImpl` type from signature arguments.
672template <typename... Args>
674
675template <VoidSignature FirstSig, typename... RestSigs>
676 requires(VoidSignature<RestSigs> && ...)
677struct CallableBufferArrayDeducer<FirstSig, RestSigs...> {
678 using type =
680};
681
682template <typename Alloc, VoidSignature FirstSig, typename... RestSigs>
683 requires InstantiatedAllocator<Alloc> && (VoidSignature<RestSigs> && ...)
684struct CallableBufferArrayDeducer<Alloc, FirstSig, RestSigs...> {
685 using type = CallableBufferArrayImpl<Alloc, FirstSig, RestSigs...>;
686};
687
688} // namespace details
689
690/**
691 * @brief Inline storage for heterogeneous callable instances with type-erased
692 * invocation.
693 * @details Stores callable instances of different types in a single contiguous
694 * buffer with embedded function pointers for type-safe invocation. Optimized
695 * for fast sequential iteration without virtual dispatch or per-callable heap
696 * allocations.
697 *
698 * The allocator parameter is optional. If the first template argument is a
699 * function signature (`void(Args...)`), the default allocator is used.
700 * Otherwise, the first argument is treated as an allocator.
701 *
702 * @tparam Args Either signatures only, or allocator followed by signatures
703 *
704 * @code
705 * // Single operation with default allocator (most common usage)
706 * CallableBufferArray<void(World&)> commands;
707 * commands.Push(SpawnEntityCmd{entity});
708 * commands.Invoke(world);
709 *
710 * // Multiple operations
711 * CallableBufferArray<void(World&), void(Logger&)> multi;
712 * multi.Push<&Cmd::Execute, &Cmd::Log>(Cmd{data});
713 * multi.Invoke<0>(world);
714 * multi.Invoke<1>(logger);
715 * @endcode
716 */
717template <typename... Args>
719 typename details::CallableBufferArrayDeducer<Args...>::type;
720
721/**
722 * @brief Inline storage for heterogeneous callable instances with type-erased
723 * invocation with polymorphic allocator.
724 * @details Stores callable instances of different types in a single contiguous
725 * buffer with embedded function pointers for type-safe invocation. Optimized
726 * for fast sequential iteration without virtual dispatch or per-callable heap
727 * allocations.
728 *
729 * The allocator parameter is optional. If the first template argument is a
730 * function signature (`void(Args...)`), the default allocator is used.
731 * Otherwise, the first argument is treated as an allocator.
732 *
733 * @tparam Args Function signatures in the form void(Args...) for the operations
734 * to support.
735 *
736 * @code
737 * // Single operation
738 * PmrCallableBufferArray<void(World&)> commands(&resource);
739 * commands.Push(SpawnEntityCmd{entity});
740 * commands.Invoke(world);
741 *
742 * // Multiple operations
743 * PmrCallableBufferArray<void(World&), void(Logger&)> multi(&resource);
744 * multi.Push<&Cmd::Execute, &Cmd::Log>(Cmd{data});
745 * multi.Invoke<0>(world);
746 * multi.Invoke<1>(logger);
747 * @endcode
748 */
749template <typename... Signatures>
752 Signatures...>;
753
754} // namespace helios::container
Implementation class for an inline callable array buffer with explicit allocator.
void Invoke(UArgs &&... args) noexcept
Invokes operation N on all stored callables in insertion order.
constexpr CallableBufferArrayImpl(const allocator_type &alloc)
Constructs with a custom allocator.
friend void swap(CallableBufferArrayImpl &lhs, CallableBufferArrayImpl &rhs) noexcept(std::is_nothrow_swappable_v< BufferType > &&std::is_nothrow_swappable_v< OffsetsType >)
constexpr void ReserveBytes(size_type bytes)
Reserves bytes in the internal buffer.
constexpr allocator_type GetAllocator() const noexcept
Gets the allocator used by the array.
constexpr bool Empty() const noexcept
Checks if the array is empty.
std::allocator_traits< allocator_type >::template rebind_alloc< std::byte > byte_allocator_type
constexpr size_type Size() const noexcept
Gets the number of stored callables.
constexpr CallableBufferArrayImpl(CallableBufferArrayImpl &&other) noexcept
constexpr CallableBufferArrayImpl(const CallableBufferArrayImpl &)=delete
CallableBufferArrayImpl(std::nullptr_t)=delete
void Push(T &&callable)
Pushes a callable with custom member function pointers.
constexpr void Reserve(size_type count)
Reserves space for approximately count callables.
CallableBufferArrayImpl & operator=(CallableBufferArrayImpl &&other) noexcept
std::allocator_traits< allocator_type >::template rebind_alloc< size_t > offset_allocator_type
constexpr size_type CapacityBytes() const noexcept
Gets the current capacity in bytes of the internal buffer.
constexpr void Swap(CallableBufferArrayImpl &other) noexcept(std::is_nothrow_swappable_v< BufferType > &&std::is_nothrow_swappable_v< OffsetsType >)
Swaps contents with another array.
constexpr void Merge(CallableBufferArrayImpl &&other)
Merges another array into this one by moving its callables.
CallableBufferArrayImpl & operator=(const CallableBufferArrayImpl &)=delete
constexpr CallableBufferArrayImpl(std::pmr::memory_resource *resource)
Constructs with a PMR memory resource.
CallableBufferArrayImpl< std::pmr::polymorphic_allocator< std::byte >, Signatures... > PmrCallableBufferArray
Inline storage for heterogeneous callable instances with type-erased invocation with polymorphic allo...
typename details::CallableBufferArrayDeducer< Args... >::type CallableBufferArray
Inline storage for heterogeneous callable instances with type-erased invocation.
constexpr size_t AlignUp(size_t value, size_t alignment) noexcept
Aligns value up to alignment.
Definition common.hpp:226
STL namespace.
CallableBufferArrayImpl< std::allocator< std::byte >, FirstSig, RestSigs... > type
Deduces the CallableBufferArrayImpl type from signature arguments.