Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
typed_buffer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/assert.hpp>
4#include <helios/container/details/typed_buffer_common.hpp>
5
6#include <concepts>
7#include <cstddef>
8#include <cstring>
9#include <memory>
10#include <memory_resource>
11#include <new>
12#include <span>
13#include <type_traits>
14#include <utility>
15#include <vector>
16
17namespace helios::container {
18
19/**
20 * @brief Type-erased single-instance byte storage.
21 * @details Stores exactly one instance of any `TypedBufferStorable` type in a
22 * `std::vector<std::byte>` backing buffer. The stored type is not fixed at
23 * class instantiation; it is set at runtime and verified on every access.
24 * Object lifetime is properly managed. Trivially-copyable types are
25 * fast-pathed.
26 * @tparam Allocator The allocator type for the byte storage (default:
27 * `std::allocator<std::byte>`)
28 */
29template <typename Allocator = std::allocator<std::byte>>
31public:
32 using allocator_type = Allocator;
33 using size_type = size_t;
35
36 using StorageType = std::vector<std::byte, allocator_type>;
37
38 /// @brief Default constructor. Creates an empty buffer with no associated
39 /// type.
40 constexpr TypedBuffer() noexcept(
41 std::is_nothrow_default_constructible_v<StorageType>) = default;
42
43 /**
44 * @brief Constructs an empty buffer with a custom allocator.
45 * @param alloc Allocator instance to use
46 */
47 explicit constexpr TypedBuffer(const allocator_type& alloc) noexcept(
48 std::is_nothrow_constructible_v<StorageType, const allocator_type&>)
49 : storage_(alloc) {}
50
51 /**
52 * @brief Constructs an empty buffer from a PMR memory resource.
53 * @details Enabled only when `allocator_type` is constructible from
54 * `std::pmr::memory_resource*`.
55 * @param resource Memory resource used to construct allocator
56 */
57 explicit constexpr TypedBuffer(std::pmr::memory_resource* resource) noexcept(
58 std::is_nothrow_constructible_v<allocator_type,
59 std::pmr::memory_resource*>)
60 requires std::constructible_from<allocator_type, std::pmr::memory_resource*>
61 : TypedBuffer(allocator_type{resource}) {}
62
63 TypedBuffer(std::nullptr_t) = delete;
64
65 /**
66 * @brief Constructs a buffer holding a value of type `T`.
67 * @tparam T The type to store
68 * @tparam Args Constructor argument types
69 * @param tag In-place type construction tag (use `std::in_place_type<T>`)
70 * @param args Arguments forwarded to `T`'s constructor
71 *
72 * @code
73 * TypedBuffer buf(std::in_place_type<int>, 42);
74 * @endcode
75 */
76 template <TypedBufferStorable T, typename... Args>
77 requires std::constructible_from<T, Args...>
78 explicit constexpr TypedBuffer(std::in_place_type_t<T> tag, Args&&... args);
79
80 /**
81 * @brief Constructs a buffer holding a value of type `T` with a custom
82 * allocator.
83 * @tparam T The type to store
84 * @tparam Args Constructor argument types
85 * @param tag In-place type construction tag (use `std::in_place_type<T>`)
86 * @param alloc Allocator instance to use
87 * @param args Arguments forwarded to `T`'s constructor
88 */
89 template <TypedBufferStorable T, typename... Args>
90 requires std::constructible_from<T, Args...>
91 constexpr TypedBuffer(std::in_place_type_t<T> tag,
92 const allocator_type& alloc, Args&&... args);
93 constexpr TypedBuffer(const TypedBuffer& other);
94 constexpr TypedBuffer(TypedBuffer&& other) noexcept(
95 std::is_nothrow_move_constructible_v<StorageType>);
96 constexpr ~TypedBuffer() noexcept { Destroy(); }
97
98 constexpr TypedBuffer& operator=(const TypedBuffer& other);
99 constexpr TypedBuffer& operator=(TypedBuffer&& other) noexcept(
100 std::is_nothrow_move_assignable_v<StorageType>);
101
102 /**
103 * @brief Changes the stored type, destroying the current value if any.
104 * @details After this call `Empty()` is true and `IsType<T>()` is true.
105 * The underlying memory is retained for reuse.
106 * @tparam T The new type
107 */
108 template <TypedBufferStorable T>
109 constexpr void ChangeType() noexcept;
110
111 /**
112 * @brief Destroys the stored value and resets type information.
113 * @details After this call both `Empty()` and `!HasType()` are true.
114 */
115 constexpr void Reset() noexcept;
116
117 /**
118 * @brief Constructs (or replaces) the stored value in-place.
119 * @details If a value of a different type is already stored it is destroyed
120 * first. The type is updated to `T` before construction.
121 * @tparam T The type to store
122 * @tparam Args Constructor argument types
123 * @param args Arguments forwarded to `T`'s constructor
124 * @return Reference to the newly constructed value
125 */
126 template <TypedBufferStorable T, typename... Args>
127 requires std::constructible_from<T, Args...>
128 T& Set(Args&&... args);
129
130 /**
131 * @brief Swaps contents with another TypedBuffer.
132 * @param other Buffer to swap with
133 */
134 constexpr void Swap(TypedBuffer& other) noexcept(
135 std::is_nothrow_swappable_v<StorageType>);
136
137 friend constexpr void swap(TypedBuffer& lhs, TypedBuffer& rhs) noexcept(
138 std::is_nothrow_swappable_v<StorageType>) {
139 lhs.Swap(rhs);
140 }
141
142 /**
143 * @brief Returns true if no value is currently stored.
144 * @return True if empty, false if a value is stored
145 */
146 [[nodiscard]] constexpr bool Empty() const noexcept { return !has_value_; }
147
148 /**
149 * @brief Returns true if a type has been associated with this buffer.
150 * @return True if a type is set, false if no type information is present
151 */
152 [[nodiscard]] constexpr bool HasType() const noexcept {
153 return type_info_.IsValid();
154 }
155
156 /**
157 * @brief Checks if the stored type matches `T`.
158 * @tparam T The type to check against
159 * @return true if types match or no type is set
160 */
161 template <TypedBufferStorable T>
162 [[nodiscard]] constexpr bool IsType() const noexcept {
163 return !type_info_.IsValid() || type_info_.type_index == TypeIndexOf<T>();
164 }
165
166 /**
167 * @brief Gets the compile-time type index for `T`.
168 * @tparam T The type to get index for
169 * @return Type index
170 */
171 template <TypedBufferStorable T>
172 [[nodiscard]] static consteval TypeIndex TypeIndexOf() noexcept {
174 }
175
176 /**
177 * @brief Gets the current stored type index.
178 * @return Type index of the stored type, or an invalid index if no type is
179 * set
180 */
181 [[nodiscard]] constexpr TypeIndex StoredTypeId() const noexcept {
182 return type_info_.type_index;
183 }
184
185 /**
186 * @brief Returns the size in bytes of the stored element type, or 0 if no
187 * type is set.
188 * @return Size of the stored type in bytes
189 */
190 [[nodiscard]] constexpr size_type ElementSize() const noexcept {
191 return type_info_.element_size;
192 }
193
194 /**
195 * @brief Accesses the stored value.
196 * @warning Triggers assertion if the buffer is empty or the stored type
197 * doesn't match `T`.
198 * @tparam T The expected type
199 * @return Reference to the stored value
200 */
201 template <TypedBufferStorable T>
202 [[nodiscard]] T& Value() noexcept;
203
204 /**
205 * @brief Accesses the stored value (const).
206 * @warning Triggers assertion if the buffer is empty or the stored type
207 * doesn't match `T`.
208 * @tparam T The expected type
209 * @return Const reference to the stored value
210 */
211 template <TypedBufferStorable T>
212 [[nodiscard]] const T& Value() const noexcept;
213
214 /**
215 * @brief Returns a const byte span of the stored value (empty if no value is
216 * stored).
217 * @return Byte span of the stored value
218 */
219 [[nodiscard]] constexpr auto Bytes() const noexcept
220 -> std::span<const std::byte>;
221
222private:
223 using TypeInfo = details::TypeBufferInfo;
224
225 template <TypedBufferStorable T>
226 [[nodiscard]] T* DataPtr() noexcept;
227
228 template <TypedBufferStorable T>
229 [[nodiscard]] const T* DataPtr() const noexcept;
230
231 [[nodiscard]] constexpr void* RawDataPtr() noexcept {
232 return storage_.empty() ? nullptr : static_cast<void*>(storage_.data());
233 }
234
235 [[nodiscard]] constexpr const void* RawDataPtr() const noexcept {
236 return storage_.empty() ? nullptr
237 : static_cast<const void*>(storage_.data());
238 }
239
240 /// @brief Destroys the stored value if present; sets has_value_ to false.
241 constexpr void Destroy() noexcept;
242
243 TypeInfo type_info_{};
244 StorageType storage_;
245 bool has_value_ = false;
246};
247
248template <typename Allocator>
249template <TypedBufferStorable T, typename... Args>
250 requires std::constructible_from<T, Args...>
251constexpr TypedBuffer<Allocator>::TypedBuffer(std::in_place_type_t<T> /*tag*/,
252 Args&&... args)
253 : type_info_(TypeInfo::template From<T>()), has_value_(true) {
254 storage_.resize(sizeof(T));
255 std::construct_at(std::launder(reinterpret_cast<T*>(storage_.data())),
256 std::forward<Args>(args)...);
257}
258
259template <typename Allocator>
260template <TypedBufferStorable T, typename... Args>
261 requires std::constructible_from<T, Args...>
262constexpr TypedBuffer<Allocator>::TypedBuffer(std::in_place_type_t<T> /*tag*/,
263 const allocator_type& alloc,
264 Args&&... args)
265 : type_info_(TypeInfo::template From<T>()),
266 storage_(alloc),
267 has_value_(true) {
268 storage_.resize(sizeof(T));
269 std::construct_at(std::launder(reinterpret_cast<T*>(storage_.data())),
270 std::forward<Args>(args)...);
271}
272
273template <typename Allocator>
275 : storage_(other.storage_.get_allocator()) {
276 if (!other.has_value_) {
277 type_info_ = other.type_info_;
278 return;
279 }
280
281 type_info_ = other.type_info_;
283 type_info_.is_copy_constructible,
284 "Cannot copy TypedBuffer: stored type is not copy constructible!");
285
286 storage_.resize(type_info_.element_size);
287
288 if (type_info_.is_trivially_copyable) {
289 std::memcpy(storage_.data(), other.storage_.data(),
290 type_info_.element_size);
291 } else {
292 type_info_.copy_construct(storage_.data(), other.storage_.data(), 1);
293 }
294
295 has_value_ = true;
296}
297
298template <typename Allocator>
300 std::is_nothrow_move_constructible_v<StorageType>)
301 : type_info_(other.type_info_),
302 storage_(std::move(other.storage_)),
303 has_value_(other.has_value_) {
304 other.has_value_ = false;
305 other.type_info_.Reset();
306}
307
308template <typename Allocator>
310 -> TypedBuffer& {
311 if (this == &other) [[unlikely]] {
312 return *this;
313 }
314
315 Destroy();
316
317 if (!other.has_value_) {
318 type_info_ = other.type_info_;
319 return *this;
320 }
321
322 type_info_ = other.type_info_;
324 type_info_.is_copy_constructible,
325 "Cannot copy-assign TypedBuffer: stored type is not copy constructible!");
326
327 if (storage_.size() < type_info_.element_size) {
328 storage_.resize(type_info_.element_size);
329 }
330
331 if (type_info_.is_trivially_copyable) {
332 std::memcpy(storage_.data(), other.storage_.data(),
333 type_info_.element_size);
334 } else {
335 type_info_.copy_construct(storage_.data(), other.storage_.data(), 1);
336 }
337
338 has_value_ = true;
339
340 return *this;
341}
342
343template <typename Allocator>
344constexpr auto TypedBuffer<Allocator>::operator=(TypedBuffer&& other) noexcept(
345 std::is_nothrow_move_assignable_v<StorageType>) -> TypedBuffer& {
346 if (this == &other) [[unlikely]] {
347 return *this;
348 }
349
350 Destroy();
351 type_info_ = other.type_info_;
352 storage_ = std::move(other.storage_);
353 has_value_ = other.has_value_;
354 other.has_value_ = false;
355 other.type_info_.Reset();
356
357 return *this;
358}
359
360template <typename Allocator>
361template <TypedBufferStorable T>
362constexpr void TypedBuffer<Allocator>::ChangeType() noexcept {
363 Destroy();
364 type_info_ = TypeInfo::template From<T>();
365}
366
367template <typename Allocator>
368constexpr void TypedBuffer<Allocator>::Reset() noexcept {
369 Destroy();
370 storage_.clear();
371 type_info_.Reset();
372}
373
374template <typename Allocator>
375template <TypedBufferStorable T, typename... Args>
376 requires std::constructible_from<T, Args...>
377inline T& TypedBuffer<Allocator>::Set(Args&&... args) {
378 Destroy();
379
380 type_info_ = TypeInfo::template From<T>();
381 if (storage_.size() < sizeof(T)) {
382 storage_.resize(sizeof(T));
383 }
384
385 std::construct_at(std::launder(reinterpret_cast<T*>(storage_.data())),
386 std::forward<Args>(args)...);
387 has_value_ = true;
388 return *std::launder(std::launder(reinterpret_cast<T*>(storage_.data())));
389}
390
391template <typename Allocator>
392constexpr void TypedBuffer<Allocator>::Swap(TypedBuffer& other) noexcept(
393 std::is_nothrow_swappable_v<StorageType>) {
394 std::swap(type_info_, other.type_info_);
395 std::swap(storage_, other.storage_);
396 std::swap(has_value_, other.has_value_);
397}
398
399template <typename Allocator>
400template <TypedBufferStorable T>
401inline T& TypedBuffer<Allocator>::Value() noexcept {
402 HELIOS_ASSERT(has_value_, "TypedBuffer::Value: buffer is empty!");
403 HELIOS_ASSERT(type_info_.type_index == TypeIndexOf<T>(),
404 "TypedBuffer::Value: type mismatch!");
405 return *std::launder(reinterpret_cast<T*>(storage_.data()));
406}
407
408template <typename Allocator>
409template <TypedBufferStorable T>
410inline const T& TypedBuffer<Allocator>::Value() const noexcept {
411 HELIOS_ASSERT(has_value_, "TypedBuffer::Value: buffer is empty!");
412 HELIOS_ASSERT(type_info_.type_index == TypeIndexOf<T>(),
413 "TypedBuffer::Value: type mismatch!");
414 return *std::launder(reinterpret_cast<const T*>(storage_.data()));
415}
416
417template <typename Allocator>
418constexpr auto TypedBuffer<Allocator>::Bytes() const noexcept
419 -> std::span<const std::byte> {
420 if (!has_value_) {
421 return {};
422 }
423 return {storage_.data(), type_info_.element_size};
424}
425
426template <typename Allocator>
427template <TypedBufferStorable T>
428inline T* TypedBuffer<Allocator>::DataPtr() noexcept {
430 !type_info_.IsValid() || type_info_.type_index == TypeIndexOf<T>(),
431 "TypedBuffer::DataPtr: type mismatch!");
432 if (storage_.empty()) [[unlikely]] {
433 return nullptr;
434 }
435 return std::launder(reinterpret_cast<T*>(storage_.data()));
436}
437
438template <typename Allocator>
439template <TypedBufferStorable T>
440inline const T* TypedBuffer<Allocator>::DataPtr() const noexcept {
442 !type_info_.IsValid() || type_info_.type_index == TypeIndexOf<T>(),
443 "TypedBuffer::DataPtr: type mismatch!");
444 if (storage_.empty()) [[unlikely]] {
445 return nullptr;
446 }
447 return std::launder(reinterpret_cast<const T*>(storage_.data()));
448}
449
450template <typename Allocator>
451constexpr void TypedBuffer<Allocator>::Destroy() noexcept {
452 if (has_value_) {
453 if (type_info_.destroy != nullptr) {
454 type_info_.destroy(storage_.data(), 1);
455 }
456 has_value_ = false;
457 }
458}
459
461
462} // namespace helios::container
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Type-erased single-instance byte storage.
constexpr TypedBuffer & operator=(TypedBuffer &&other) noexcept(std::is_nothrow_move_assignable_v< StorageType >)
constexpr bool IsType() const noexcept
Checks if the stored type matches T.
static consteval TypeIndex TypeIndexOf() noexcept
Gets the compile-time type index for T.
T & Value() noexcept
Accesses the stored value.
constexpr void ChangeType() noexcept
Changes the stored type, destroying the current value if any.
constexpr bool HasType() const noexcept
Returns true if a type has been associated with this buffer.
std::vector< std::byte, allocator_type > StorageType
constexpr void Swap(TypedBuffer &other) noexcept(std::is_nothrow_swappable_v< StorageType >)
constexpr TypedBuffer(const TypedBuffer &other)
constexpr bool Empty() const noexcept
Returns true if no value is currently stored.
constexpr TypedBuffer() noexcept(std::is_nothrow_default_constructible_v< StorageType >)=default
Default constructor. Creates an empty buffer with no associated type.
constexpr TypedBuffer(TypedBuffer &&other) noexcept(std::is_nothrow_move_constructible_v< StorageType >)
constexpr TypedBuffer(std::pmr::memory_resource *resource) noexcept(std::is_nothrow_constructible_v< allocator_type, std::pmr::memory_resource * >)
Constructs an empty buffer from a PMR memory resource.
constexpr ~TypedBuffer() noexcept
constexpr size_type ElementSize() const noexcept
Returns the size in bytes of the stored element type, or 0 if no type is set.
constexpr TypedBuffer(std::in_place_type_t< T > tag, const allocator_type &alloc, Args &&... args)
Constructs a buffer holding a value of type T with a custom allocator.
constexpr auto Bytes() const noexcept -> std::span< const std::byte >
constexpr TypedBuffer(std::in_place_type_t< T > tag, Args &&... args)
Constructs a buffer holding a value of type T.
constexpr TypeIndex StoredTypeId() const noexcept
Gets the current stored type index.
TypedBuffer(std::nullptr_t)=delete
friend constexpr void swap(TypedBuffer &lhs, TypedBuffer &rhs) noexcept(std::is_nothrow_swappable_v< StorageType >)
constexpr TypedBuffer & operator=(const TypedBuffer &other)
static constexpr TypeIndex From() noexcept
Constructs a TypeIndex from a type T.
TypedBuffer< std::pmr::polymorphic_allocator< std::byte > > PmrTypedBuffer
STL namespace.