diff options
| author | Felix Morgner <felix.morgner@ost.ch> | 2026-03-19 07:16:58 +0100 |
|---|---|---|
| committer | Felix Morgner <felix.morgner@ost.ch> | 2026-03-19 07:16:58 +0100 |
| commit | c049d767c7d41d04226f2c6b01ee2e07aae7ea1f (patch) | |
| tree | 7bd365a53df9a341d698a370ebf50b2e04ccfc40 /libs | |
| parent | c86c898836b1564de2733330540f5d0cd56e8b10 (diff) | |
| download | teachos-c049d767c7d41d04226f2c6b01ee2e07aae7ea1f.tar.xz teachos-c049d767c7d41d04226f2c6b01ee2e07aae7ea1f.zip | |
kstd: prepare vector to be allocator aware
Diffstat (limited to 'libs')
| -rw-r--r-- | libs/kstd/include/kstd/vector | 426 |
1 files changed, 289 insertions, 137 deletions
diff --git a/libs/kstd/include/kstd/vector b/libs/kstd/include/kstd/vector index 6af7c12..b87756a 100644 --- a/libs/kstd/include/kstd/vector +++ b/libs/kstd/include/kstd/vector @@ -6,6 +6,10 @@ #include <algorithm> #include <cstddef> #include <initializer_list> +#include <iterator> +#include <memory> +#include <ranges> +#include <utility> namespace kstd { @@ -14,129 +18,254 @@ namespace kstd * custom memory management. * * @tparam T Element the vector instance should contain. + * @tparam Allocator The allocator to use when allocating new elements. */ - template<typename T> + template<typename T, typename Allocator = std::allocator<T>> struct vector { using value_type = T; ///< Type of the elements contained in the container. + using allocator_type = Allocator; ///< Type of the allocator used by the container. using size_type = std::size_t; ///< Type of the size in the container. + using difference_type = std::ptrdiff_t; ///< Type of the difference between two iterators. using reference = value_type &; ///< Type of reference to the elements. using const_reference = value_type const &; ///< Type of constant reference to the elements. using pointer = value_type *; ///< Type of pointer to the elements. using const_pointer = value_type const *; ///< Type of constant pointer to the elements. + using iterator = pointer; ///< Type of iterator to the elements. + using const_iterator = const_pointer; ///< Type of constant iterator to the elements. + using reverse_iterator = std::reverse_iterator<iterator>; + using const_reverse_iterator = std::reverse_iterator<const_iterator>; - /** - * @brief Default Constructor. - */ + //! Construct a new, empty vector. vector() = default; - /** - * @brief Constructs data with the given amount of elements containing the given value or alternatively the default - * constructed value. - * - * @param n Amount of elements we want to create and set the given value for. - * @param initial Inital value of all elements in the underlying data array. - */ + //! Construct a new vector, with the given number of element and initialize them to the given value. + //! + //! @param n The number of elements to construct the container with. + //! @param initial The value to initialize each element to. explicit vector(size_type n, value_type initial = value_type{}) - : _size(n) - , _capacity(n) - , _data(_capacity ? new value_type[_capacity]{} : nullptr) + : m_size{n} + , m_capacity{n} + , m_data{allocate_n(m_capacity)} { - std::ranges::fill(*this, initial); + for (auto i = 0uz; i < n; ++i) + { + std::allocator_traits<allocator_type>::construct(m_allocator, m_data + i, initial); + } } - /** - * @brief Constructs data by copying all element from the given exclusive range. - * - * @tparam InputIterator Template that should have atleast input iterator characteristics. - * @param first Input iterator to the first element in the range we want to copy from. - * @param last Input iterator to one past the last element in the range we want to copy from. - */ + //! Construct a new vector and initialize it's content by copying all elements in the given range. + //! + //! @tparam InputIterator An iterator type used to describe the source range. + //! @param first The start of the source range. + //! @param last The end of the source range. template<typename InputIterator> explicit vector(InputIterator first, InputIterator last) - : _size(std::distance(first, last)) - , _capacity(std::distance(first, last)) - , _data(_capacity ? new value_type[_capacity]() : nullptr) + : m_allocator{} + , m_size{std::ranges::distance(first, last)} + , m_capacity{std::ranges::distance(first, last)} + , m_data{allocate_n(m_capacity)} { - std::ranges::copy(first, last, _data); + for (auto destination = begin(); first != last; ++first, ++destination) + { + std::allocator_traits<allocator_type>::construct(m_allocator, destination, *first); + } } - /** - * @brief Construct data by copying all elements from the initializer list. - * - * @param initializer_list List we want to copy all elements from. - */ - explicit vector(std::initializer_list<value_type> initializer_list) - : _size(initializer_list.size()) - , _capacity(initializer_list.size()) - , _data(_capacity ? new value_type[_capacity]() : nullptr) - { - std::ranges::copy(initializer_list, _data); + //! Construct a new vector and initialize it's content by copying all elements in the given initializer list. + //! + //! @param list The initializer list containing the source objects. + explicit vector(std::initializer_list<value_type> list) + : vector{std::ranges::begin(list), std::ranges::end(list)} + {} + + //! Construct a new vector and initialize it's content by copying all elements from a given vector. + //! + //! @param other The source vector. + vector(vector const & other) + : m_allocator{other.m_allocator} + , m_size{other.m_size} + , m_capacity{other.m_capacity} + , m_data(allocate_n(m_capacity)) + { + auto first = std::ranges::begin(other); + auto last = std::ranges::end(other); + for (auto destination = begin(); first != last; ++first, ++destination) + { + std::allocator_traits<allocator_type>::construct(m_allocator, destination, *first); + } } - /** - * @brief Copy constructor. - * - * @note Allocates underlying data container with the same capacity as vector we are copying from and copies all - * elements from it. - * - * @param other Other instance of vector we want to copy the data from. - */ - vector(vector<value_type> const & other) - : _size(other._size) - , _capacity(other._capacity) - , _data(_capacity ? new value_type[_capacity]() : nullptr) + //! Construct a new vector and initialize it's content by moving from a given vector. + //! + //! @param other The source vector. + vector(vector && other) noexcept + : m_allocator{std::exchange(other.m_allocator, allocator_type{})} + , m_size{std::exchange(other.m_size, size_type{})} + , m_capacity(std::exchange(other.m_capacity, size_type{})) + , m_data(std::exchange(other.m_data, nullptr)) + {} + + //! Destroy this vector. + ~vector() { - std::ranges::copy(other, _data); + std::ranges::for_each(std::views::reverse(*this), [&](auto & element) { + std::allocator_traits<allocator_type>::destroy(m_allocator, std::addressof(element)); + }); + std::allocator_traits<allocator_type>::deallocate(m_allocator, m_data, m_capacity); } - /** - * @brief Copy assignment operator. - * - * @note Allocates underlying data container with the same capacity as vector we are copying from and copies all - * elements from it. - * - * @param other Other instance of vector we want to copy the data from. - * @return Newly created copy. - */ + //! Replace the contents of this vector by the copying from the given source vector. + //! + //! @param other The source vector. auto operator=(vector const & other) -> vector<value_type> & { - delete[] _data; - _size = other._size; - _capacity = other._capacity; - _data = _capacity ? new value_type[_capacity]() : nullptr; - std::ranges::copy(other, _data); + if (this == std::addressof(other)) + { + return *this; + } + + if constexpr (std::allocator_traits<allocator_type>::propagate_on_container_move_assignment::value) + { + if (m_allocator != other.m_allocator) + { + clear_and_deallocate(); + } + m_allocator = other.m_allocator; + } + + if (m_capacity >= other.m_size) + { + auto const overlap = std::min(m_size, other.m_size); + auto first = std::ranges::begin(other); + for (auto i = 0uz; i < overlap; ++first, ++i) + { + m_data[i] = *first; + } + + if (m_size < other.m_size) + { + for (auto i = m_size; i < other.m_size; ++first, ++i) + { + std::allocator_traits<allocator_type>::construct(m_allocator, m_data + i, *first); + } + } + else if (m_size > other.m_size) + { + for (auto i = other.m_size; i < m_size; ++i) + { + std::allocator_traits<allocator_type>::destroy(m_allocator, m_data + i); + } + } + } + else + { + auto new_data = std::allocator_traits<allocator_type>::allocate(m_allocator, other.m_size); + for (auto i = 0uz; i < other.m_size; ++i) + { + std::allocator_traits<allocator_type>::construct(m_allocator, new_data + i, other.m_data[i]); + } + clear_and_deallocate(); + std::exchange(m_data, new_data); + m_capacity = other.m_size; + } + + m_size = other.m_size; return *this; } - /** - * @brief Destructor. - */ - ~vector() + //! Replace the contents fo this vector by moving from the given source. + //! + //! @param other The source vector. + auto operator=(vector && other) noexcept -> vector<value_type> & { - delete[] _data; + if (this == std::addressof(other)) + { + return *this; + } + + if constexpr (std::allocator_traits<allocator_type>::propagate_on_container_move_assignment::value) + { + clear_and_deallocate(); + m_allocator = std::move(other.m_allocator); + m_size = std::exchange(other.m_size, size_type{}); + m_capacity = std::exchange(other.m_capacity, size_type{}); + m_data = std::exchange(other.m_data, nullptr); + } + else if (m_allocator != other.m_allocator) + { + clear_and_deallocate(); + m_size = std::exchange(other.m_size, size_type{}); + m_capacity = std::exchange(other.m_capacity, size_type{}); + m_data = std::exchange(other.m_data, nullptr); + } + else + { + if (m_capacity >= other.m_size) + { + auto const overlap = std::min(m_size, other.m_size); + auto first = std::ranges::begin(other); + for (auto i = 0uz; i < overlap; ++first, ++i) + { + m_data[i] = std::move(*first); + } + + if (m_size < other.m_size) + { + for (auto i = m_size; i < other.m_size; ++first, ++i) + { + std::allocator_traits<allocator_type>::construct(m_allocator, m_data + i, std::move(*first)); + } + } + else if (m_size > other.m_size) + { + for (auto i = other.m_size; i < m_size; ++i) + { + std::allocator_traits<allocator_type>::destroy(m_allocator, m_data + i); + } + } + } + else + { + auto new_data = std::allocator_traits<allocator_type>::allocate(m_allocator, other.m_size); + for (auto i = 0uz; i < other.m_size; ++i) + { + std::allocator_traits<allocator_type>::destroy(m_allocator, m_data + i); + } + clear_and_deallocate(); + std::exchange(m_data, new_data); + m_capacity = other.m_size; + } + for (auto i = 0uz; i < other.m_size; ++i) + { + std::allocator_traits<allocator_type>::destroy(other.m_allocator, other.m_data + i); + } + m_size = std::exchange(other.m_size, size_type{}); + } + + return *this; } /** - * @brief Amount of elements currently contained in this vector, will fill up until we have reached the capacity. If - * that is the case the capacity is increased automatically. + * @brief Amount of elements currently contained in this vector, will fill up until we have reached the capacity. + * If that is the case the capacity is increased automatically. * * @return Current amount of elements. */ [[nodiscard]] auto size() const -> size_type { - return _size; + return m_size; } /** - * @brief Amount of space the vector currently has, can be different than the size, because we allocate more than we - * exactly require to decrease the amount of allocations and deallocation to improve speed. + * @brief Amount of space the vector currently has, can be different than the size, because we allocate more than + * we exactly require to decrease the amount of allocations and deallocation to improve speed. * * @return Current amount of space the vector has for elements. */ [[nodiscard]] auto capacity() const -> size_type { - return _capacity; + return m_capacity; } /** @@ -149,7 +278,7 @@ namespace kstd */ auto operator[](size_type index) -> reference { - return _data[index]; + return m_data[index]; } /** @@ -162,13 +291,14 @@ namespace kstd */ auto operator[](size_type index) const -> const_reference { - return _data[index]; + return m_data[index]; } /** * @brief Array indexing operator. Allowing to access element at the given index. * - * @note Ensures we do not access element outside of the bounds of the array, if we do further execution is halted. + * @note Ensures we do not access element outside of the bounds of the array, if we do further execution is + * halted. * * @param index Index we want to access elements at. * @return Reference to the underlying element. @@ -176,13 +306,14 @@ namespace kstd auto at(size_type index) -> reference { throw_if_out_of_range(index); - return this->operator[](index); + return (*this)[index]; } /** * @brief Array indexing operator. Allowing to access element at the given index. * - * @note Ensures we do not access element outside of the bounds of the array, if we do further execution is halted. + * @note Ensures we do not access element outside of the bounds of the array, if we do further execution is + * halted. * * @param index Index we want to access elements at. * @return Reference to the underlying element. @@ -190,7 +321,7 @@ namespace kstd [[nodiscard]] auto at(size_type index) const -> const_reference { throw_if_out_of_range(index); - return this->operator[](index); + return (*this)[index]; } /** @@ -209,13 +340,13 @@ namespace kstd auto push_back(U && value) -> void { increase_capacity_if_full(); - _data[_size] = std::forward<U>(value); - (void)_size++; + std::allocator_traits<allocator_type>::construct(m_allocator, &(*this)[m_size], std::forward<U>(value)); + ++m_size; } /** - * @brief Appends a new element to the end of the container. The element is constructed through a constructor of the - * template type. The arguments args... are forwarded to the constructor as std::forward<Args>(args).... + * @brief Appends a new element to the end of the container. The element is constructed through a constructor of + * the template type. The arguments args... are forwarded to the constructor as std::forward<Args>(args).... * * If after the operation the new size() is greater than old capacity() a reallocation takes place, in which case * all iterators (including the end() iterator) and all references to the elements are invalidated. Otherwise only @@ -230,9 +361,9 @@ namespace kstd auto emplace_back(Args &&... args) -> value_type & { increase_capacity_if_full(); - _data[_size] = value_type{std::forward<Args>(args)...}; - auto const index = _size++; - return _data[index]; + std::allocator_traits<allocator_type>::construct(m_allocator, &(*this)[m_size], std::forward<Args>(args)...); + ++m_size; + return this->back(); } /** @@ -243,7 +374,7 @@ namespace kstd auto pop_back() -> void { throw_if_empty(); - (void)_size--; + (void)m_size--; } /** @@ -254,7 +385,7 @@ namespace kstd */ auto begin() noexcept -> pointer { - return _data; + return m_data; } /** @@ -265,7 +396,7 @@ namespace kstd */ [[nodiscard]] auto begin() const noexcept -> const_pointer { - return _data; + return m_data; } /** @@ -280,30 +411,30 @@ namespace kstd } /** - * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last element - * of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). + * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last + * element of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). * * @return Reverse iterator to the first element. */ auto rbegin() noexcept -> pointer { - return _data + _size - 1; + return m_data + m_size - 1; } /** - * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last element - * of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). + * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last + * element of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). * * @return Reverse iterator to the first element. */ [[nodiscard]] auto rbegin() const noexcept -> const_pointer { - return _data + _size - 1; + return m_data + m_size - 1; } /** - * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last element - * of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). + * @brief Returns a reverse iterator to the first element of the reversed vector. It corresponds to the last + * element of the non-reversed vector. If the vector is empty, the returned iterator will be equal to rend(). * * @return Reverse iterator to the first element. */ @@ -320,7 +451,7 @@ namespace kstd */ auto end() noexcept -> pointer { - return _data + _size; + return m_data + m_size; } /** @@ -331,7 +462,7 @@ namespace kstd */ [[nodiscard]] auto end() const noexcept -> const_pointer { - return _data + _size; + return m_data + m_size; } /** @@ -354,7 +485,7 @@ namespace kstd */ auto rend() noexcept -> pointer { - return _data + size() - 1; + return m_data + size() - 1; } /** @@ -366,7 +497,7 @@ namespace kstd */ [[nodiscard]] auto rend() const noexcept -> const_pointer { - return _data + size() - 1; + return m_data + size() - 1; } /** @@ -383,28 +514,28 @@ namespace kstd /** * @brief Returns a pointer to the underlying array serving as element storage. The pointer is such that range - * [data(), data() + size()) is always a valid range, even if the container is empty (data() is not dereferenceable - * in that case). + * [data(), data() + size()) is always a valid range, even if the container is empty (data() is not + * dereferenceable in that case). * - * @return Pointer to the underlying element storage. For non-empty containers, the returned pointer compares equal - * to the address of the first element. + * @return Pointer to the underlying element storage. For non-empty containers, the returned pointer compares + * equal to the address of the first element. */ auto data() -> pointer { - return _data; + return m_data; } /** * @brief Returns a pointer to the underlying array serving as element storage. The pointer is such that range - * [data(), data() + size()) is always a valid range, even if the container is empty (data() is not dereferenceable - * in that case). + * [data(), data() + size()) is always a valid range, even if the container is empty (data() is not + * dereferenceable in that case). * - * @return Pointer to the underlying element storage. For non-empty containers, the returned pointer compares equal - * to the address of the first element. + * @return Pointer to the underlying element storage. For non-empty containers, the returned pointer compares + * equal to the address of the first element. */ [[nodiscard]] auto data() const -> const_pointer { - return _data; + return m_data; } /** @@ -469,11 +600,11 @@ namespace kstd * the vector greater than the value of capacity(). * * @note Correctly using reserve() can prevent unnecessary reallocations, but inappropriate uses of reserve() (for - * instance, calling it before every push_back() call) may actually increase the number of reallocations (by causing - * the capacity to grow linearly rather than exponentially) and result in increased computational complexity and - * decreased performance. For example, a function that receives an arbitrary vector by reference and appends - * elements to it should usually not call reserve() on the vector, since it does not know of the vector's usage - * characteristics. + * instance, calling it before every push_back() call) may actually increase the number of reallocations (by + * causing the capacity to grow linearly rather than exponentially) and result in increased computational + * complexity and decreased performance. For example, a function that receives an arbitrary vector by reference + * and appends elements to it should usually not call reserve() on the vector, since it does not know of the + * vector's usage characteristics. * * When inserting a range, the range version of insert() is generally preferable as it preserves the correct * capacity growth behavior, unlike reserve() followed by a series of push_back()s. @@ -484,16 +615,16 @@ namespace kstd */ auto reserve(size_type new_capacity) -> void { - if (new_capacity <= _capacity) + if (new_capacity <= m_capacity) { return; } - _capacity = new_capacity; - auto temp = new value_type[_capacity](); + m_capacity = new_capacity; + auto temp = new value_type[m_capacity](); std::ranges::copy(begin(), end(), temp); - delete[] _data; - _data = temp; + delete[] m_data; + m_data = temp; } /** @@ -504,16 +635,16 @@ namespace kstd */ auto shrink_to_fit() -> void { - if (_size == _capacity) + if (m_size == m_capacity) { return; } - _capacity = _size; - auto temp = new value_type[_capacity]{}; + m_capacity = m_size; + auto temp = new value_type[m_capacity]{}; std::ranges::copy(begin(), end(), temp); - delete[] _data; - _data = temp; + delete[] m_data; + m_data = temp; } /** @@ -523,10 +654,30 @@ namespace kstd */ [[nodiscard]] auto empty() const -> bool { - return _size <= 0; + return m_size <= 0; } private: + auto allocate_n(std::size_t count) -> std::allocator_traits<allocator_type>::pointer + { + if (count) + { + return std::allocator_traits<allocator_type>::allocate(m_allocator, count); + } + return nullptr; + } + + auto clear_and_deallocate() -> void + { + std::ranges::for_each(std::views::reverse(*this), [&](auto & element) { + std::allocator_traits<allocator_type>::destroy(m_allocator, std::addressof(element)); + }); + std::allocator_traits<allocator_type>::deallocate(m_allocator, m_data, m_capacity); + m_capacity = 0; + m_size = 0; + m_data = nullptr; + } + /** * @brief Halts the execution of the application if the data container is currently empty. */ @@ -540,7 +691,7 @@ namespace kstd auto throw_if_out_of_range(size_type index) const -> void { - if (index >= _size) + if (index >= m_size) { os::panic("[Vector] Attempted to read element at invalid index"); } @@ -548,20 +699,21 @@ namespace kstd /** * @brief Increases the internal capacity to 1 if it was previously 0 and to * 2 after that, meaning exponential - * growth. This is done to decrease the amount of single allocations done and because a power of 2 in memory size is - * normally perferable for the cache. + * growth. This is done to decrease the amount of single allocations done and because a power of 2 in memory size + * is normally perferable for the cache. */ auto increase_capacity_if_full() -> void { - if (_size == _capacity) + if (m_size == m_capacity) { - reserve(_capacity == 0U ? 1U : _capacity * 2U); + reserve(m_capacity == 0U ? 1U : m_capacity * 2U); } } - size_type _size = {}; ///< Amount of elements in the underlying data container - size_type _capacity = {}; ///< Amount of space for elements in the underlying data container - value_type * _data = {}; ///< Pointer to the first element in the underlying data container + [[no_unique_address]] allocator_type m_allocator{}; + size_type m_size{}; ///< Amount of elements in the underlying data container + size_type m_capacity{}; ///< Amount of space for elements in the underlying data container + value_type * m_data{}; ///< Pointer to the first element in the underlying data container }; } // namespace kstd |
