diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 8a4413fb0..abc85beb2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -30,6 +30,7 @@ set(CUBOS_CORE_SOURCE "src/cubos/core/memory/stream.cpp" "src/cubos/core/memory/standard_stream.cpp" "src/cubos/core/memory/buffer_stream.cpp" + "src/cubos/core/memory/any_vector.cpp" "src/cubos/core/reflection/type.cpp" "src/cubos/core/reflection/traits/constructible.cpp" diff --git a/core/include/cubos/core/memory/any_vector.hpp b/core/include/cubos/core/memory/any_vector.hpp new file mode 100644 index 000000000..bb9d33424 --- /dev/null +++ b/core/include/cubos/core/memory/any_vector.hpp @@ -0,0 +1,92 @@ +/// @file +/// @brief Class @ref cubos::core::memory::UnorderedBimap. +/// @ingroup core-memory + +#pragma once + +#include + +namespace cubos::core::reflection +{ + class Type; + class ConstructibleTrait; +} // namespace cubos::core::reflection + +namespace cubos::core::memory +{ + /// @brief Stores a dynamically sized array of blobs of a given reflected type. + /// @ingroup core-memory + class AnyVector final + { + public: + ~AnyVector(); + + /// @brief Constructs with the given element type. + /// @note @p elementType must have @ref reflection::ConstructibleTrait. + /// @param elementType Element type. + AnyVector(const reflection::Type& elementType); + + /// @brief Move constructs. + /// @param other Vector. + AnyVector(AnyVector&& other) noexcept; + + /// @brief Get the type of the elements stored in the vector. + /// @return Element type. + const reflection::Type& elementType() const; + + /// @brief Reserves space for at least @p capacity elements. + /// @note Aborts if @p capacity is less than @ref size(). + /// @param capacity Minimum capacity. + void reserve(std::size_t capacity); + + /// @brief Pushes a new default-constructed element to the back of the vector. + /// @note Aborts if @ref elementType() is not default-constructible. + void pushDefault(); + + /// @brief Pushes a new copy-constructed element to the back of the vector. + /// @note Aborts if @ref elementType() is not copy-constructible. + /// @param element Element to copy. + void pushCopy(const void* value); + + /// @brief Pushes a new move-constructed element to the back of the vector. + /// @note Aborts if @ref elementType() is not move-constructible. + /// @param element Element to move. + void pushMove(void* value); + + /// @brief Removes the last element from the vector. + /// @note Aborts if the vector is empty. + void pop(); + + /// @brief Removes all elements from the vector. + void clear(); + + /// @brief Get the element at the given index. + /// @note Aborts if @p index is out of bounds. + /// @param index Index of the element to get. + /// @return Pointer to the element. + void* at(std::size_t index); + + /// @copydoc at(std::size_t) + const void* at(std::size_t index) const; + + /// @brief Get the number of elements in the vector. + /// @return Element count. + std::size_t size() const; + + /// @brief Get the number of elements the vector can hold without reallocating. + /// @return Element capacity. + std::size_t capacity() const; + + /// @brief Checks if the vector is empty. + /// @return Whether the vector is empty. + bool empty() const; + + private: + const reflection::Type& mElementType; + const reflection::ConstructibleTrait* mConstructibleTrait{nullptr}; + void* mData{nullptr}; + std::size_t mSize{0}; + std::size_t mCapacity{0}; + std::size_t mStride{0}; + }; +} // namespace cubos::core::memory diff --git a/core/src/cubos/core/memory/any_vector.cpp b/core/src/cubos/core/memory/any_vector.cpp new file mode 100644 index 000000000..636451809 --- /dev/null +++ b/core/src/cubos/core/memory/any_vector.cpp @@ -0,0 +1,142 @@ +#include + +#include +#include +#include +#include + +using cubos::core::memory::AnyVector; +using cubos::core::reflection::ConstructibleTrait; +using cubos::core::reflection::Type; + +AnyVector::~AnyVector() +{ + std::free(mData); +} + +AnyVector::AnyVector(const Type& elementType) + : mElementType(elementType) +{ + CUBOS_ASSERT(mElementType.has(), "Type must be constructible"); + mConstructibleTrait = &mElementType.get(); + + CUBOS_ASSERT(mConstructibleTrait->size() % mConstructibleTrait->alignment() == 0, + "Size must be a multiple of alignment"); + mStride = mConstructibleTrait->size(); +} + +AnyVector::AnyVector(AnyVector&& other) noexcept + : mElementType(other.mElementType) + , mConstructibleTrait(other.mConstructibleTrait) + , mData(other.mData) + , mSize(other.mSize) + , mCapacity(other.mCapacity) + , mStride(other.mStride) +{ + other.mSize = 0; + other.mCapacity = 0; + other.mData = nullptr; +} + +const Type& AnyVector::elementType() const +{ + return mElementType; +} + +void AnyVector::reserve(std::size_t capacity) +{ + CUBOS_ASSERT(capacity >= mSize, "Capacity must be greater than or equal to size"); + + if (mCapacity == capacity) + { + return; + } + + mCapacity = capacity; + mData = std::realloc(mData, mStride * mCapacity); + CUBOS_ASSERT(mData != nullptr, "Vector memory reallocation failed"); +} + +void AnyVector::pushDefault() +{ + CUBOS_ASSERT(mConstructibleTrait->hasDefaultConstruct(), "Type must be default-constructible"); + + if (mSize == mCapacity) + { + this->reserve(mCapacity == 0 ? 1 : mCapacity * 2); + } + + mConstructibleTrait->defaultConstruct(static_cast(mData) + mStride * mSize); + ++mSize; +} + +void AnyVector::pushCopy(const void* value) +{ + CUBOS_ASSERT(mConstructibleTrait->hasCopyConstruct(), "Type must be copy-constructible"); + + if (mSize == mCapacity) + { + this->reserve(mCapacity == 0 ? 1 : mCapacity * 2); + } + + mConstructibleTrait->copyConstruct(static_cast(mData) + mStride * mSize, value); + ++mSize; +} + +void AnyVector::pushMove(void* value) +{ + CUBOS_ASSERT(mConstructibleTrait->hasMoveConstruct(), "Type must be move-constructible"); + + if (mSize == mCapacity) + { + this->reserve(mCapacity == 0 ? 1 : mCapacity * 2); + } + + mConstructibleTrait->moveConstruct(static_cast(mData) + mStride * mSize, value); + ++mSize; +} + +void AnyVector::pop() +{ + CUBOS_ASSERT(mSize > 0, "Vector must not be empty"); + + --mSize; + mConstructibleTrait->destruct(static_cast(mData) + mStride * mSize); +} + +void AnyVector::clear() +{ + for (std::size_t i = 0; i < mSize; ++i) + { + mConstructibleTrait->destruct(static_cast(mData) + mStride * i); + } + + mSize = 0; +} + +void* AnyVector::at(std::size_t index) +{ + CUBOS_ASSERT(index < mSize, "Index must be less than size"); + return static_cast(mData) + mStride * index; +} + +const void* AnyVector::at(std::size_t index) const +{ + CUBOS_ASSERT(index < mSize, "Index must be less than size"); + return static_cast(mData) + mStride * index; +} + +std::size_t AnyVector::size() const +{ + return mSize; +} + +std::size_t AnyVector::capacity() const +{ + return mCapacity; +} + +bool AnyVector::empty() const +{ + return mSize == 0; +} diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index c843242ce..96a95a87c 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable( data/fs/file_system.cpp data/context.cpp + memory/any_vector.cpp memory/unordered_bimap.cpp ecs/utils.cpp diff --git a/core/tests/memory/any_vector.cpp b/core/tests/memory/any_vector.cpp new file mode 100644 index 000000000..01bc32035 --- /dev/null +++ b/core/tests/memory/any_vector.cpp @@ -0,0 +1,109 @@ +#include + +#include +#include +#include + +#include "../utils.hpp" + +TEST_CASE("memory::AnyVector") +{ + using cubos::core::memory::AnyVector; + using cubos::core::reflection::reflect; + + SUBCASE("with integers") + { + AnyVector vec{reflect()}; + CHECK(vec.elementType().is()); + + SUBCASE("pushDefault") + { + vec.pushDefault(); + CHECK(vec.size() == 1); + CHECK(vec.at(0) != nullptr); + CHECK(*static_cast(vec.at(0)) == 0); + } + + SUBCASE("pushCopy/pushMove") + { + int value = 42; + + SUBCASE("pushCopy") + { + vec.pushCopy(&value); + } + + SUBCASE("pushMove") + { + vec.pushMove(&value); + } + + CHECK(vec.size() == 1); + CHECK(vec.at(0) != nullptr); + CHECK(*static_cast(vec.at(0)) == 42); + } + + SUBCASE("move constructor") + { + constexpr std::size_t Size = 100; + + for (std::size_t i = 0; i < Size; ++i) + { + vec.pushDefault(); + *static_cast(vec.at(i)) = static_cast(i); + } + + AnyVector vec2{std::move(vec)}; + CHECK_FALSE(vec2.empty()); + CHECK(vec2.size() == Size); + + for (std::size_t i = 0; i < Size; ++i) + { + CHECK(*static_cast(vec2.at(i)) == static_cast(i)); + } + } + } + + SUBCASE("detect destructor") + { + AnyVector vec{reflect()}; + CHECK(vec.elementType().is()); + + constexpr std::size_t Size = 100; + bool destroyed[Size]; + + for (std::size_t i = 0; i < Size; ++i) + { + CHECK(vec.size() == i); + vec.pushDefault(); + CHECK(vec.size() == i + 1); + CHECK(vec.capacity() >= i + 1); + + destroyed[i] = false; + static_cast(vec.at(i))->set(&destroyed[i]); + } + + SUBCASE("pop until empty") + { + for (std::size_t i = 0; i < Size; ++i) + { + CHECK_FALSE(destroyed[Size - i - 1]); + vec.pop(); + CHECK(destroyed[Size - i - 1]); + } + } + + SUBCASE("clear") + { + vec.clear(); + } + + for (const auto& i : destroyed) + { + CHECK(i); + } + + CHECK(vec.empty()); + CHECK(vec.size() == 0); + } +} diff --git a/core/tests/utils.hpp b/core/tests/utils.hpp index 7468e2dad..898fee23c 100644 --- a/core/tests/utils.hpp +++ b/core/tests/utils.hpp @@ -90,6 +90,11 @@ class DetectDestructor } } + void set(bool* destructed) + { + mDestructed = destructed; + } + private: bool* mDestructed; };