-
Notifications
You must be signed in to change notification settings - Fork 10
Add tunable_vector std::vector wrapper container
#746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #pragma once | ||
|
|
||
| #include <foonathan/memory/default_allocator.hpp> | ||
| #include <foonathan/memory/std_allocator.hpp> | ||
|
|
||
| #include "openvic-simulation/core/memory/MemoryTracker.hpp" | ||
| #include "openvic-simulation/core/stl/containers/TunableVector.hpp" | ||
|
|
||
| namespace OpenVic::memory { | ||
| template< | ||
| typename T, ::OpenVic::stl::_detail::tunable_growth_trait GrowthTrait = ::OpenVic::stl::default_growth_traits, | ||
| class RawAllocator = foonathan::memory::default_allocator> | ||
| using tunable_vector = | ||
| ::OpenVic::stl::tunable_vector<T, GrowthTrait, foonathan::memory::std_allocator<T, tracker<RawAllocator>>>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,371 @@ | ||
| #pragma once | ||
|
|
||
| #include <algorithm> | ||
| #include <concepts> | ||
| #include <cstddef> | ||
| #include <initializer_list> | ||
| #include <iterator> | ||
| #include <memory> | ||
| #include <ranges> | ||
| #include <type_traits> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #include <range/v3/algorithm/move.hpp> | ||
|
|
||
| #include "openvic-simulation/core/Assert.hpp" | ||
| #include "openvic-simulation/core/Typedefs.hpp" | ||
|
|
||
| namespace OpenVic::stl { | ||
| template<size_t Num, size_t Denom, size_t InitAllocSize = 0> | ||
| struct growth_traits { | ||
| static constexpr std::integral_constant<size_t, Num> numerator {}; | ||
| static constexpr std::integral_constant<size_t, Denom> denominator {}; | ||
| static constexpr std::integral_constant<size_t, InitAllocSize> initial_allocation_size {}; | ||
| }; | ||
|
|
||
| // Uses approximated golden ratio (1.617...) | ||
| using default_growth_traits = growth_traits<55, 34>; | ||
|
|
||
| namespace _detail { | ||
| template<typename T> | ||
| concept tunable_growth_trait = requires() { | ||
| { T::numerator() } -> std::convertible_to<size_t>; | ||
| { T::denominator() } -> std::convertible_to<size_t>; | ||
| { T::initial_allocation_size() } -> std::convertible_to<size_t>; | ||
| }; | ||
|
|
||
| template<typename Range, typename T> | ||
| concept _compatible_range = | ||
| std::ranges::input_range<Range> && std::convertible_to<std::ranges::range_reference_t<Range>, T>; | ||
| } | ||
|
|
||
| template< | ||
| typename T, _detail::tunable_growth_trait GrowthTrait = default_growth_traits, typename Allocator = std::allocator<T>> | ||
| class tunable_vector { | ||
| public: | ||
| using value_type = T; | ||
| using allocator_type = Allocator; | ||
| using container_type = std::vector<value_type, allocator_type>; | ||
| using size_type = typename container_type::size_type; | ||
| using difference_type = typename container_type::difference_type; | ||
| using reference = typename container_type::reference; | ||
| using const_reference = typename container_type::const_reference; | ||
| using pointer = typename container_type::pointer; | ||
| using const_pointer = typename container_type::const_pointer; | ||
| using iterator = typename container_type::iterator; | ||
| using const_iterator = typename container_type::const_iterator; | ||
| using reverse_iterator = typename container_type::reverse_iterator; | ||
| using const_reverse_iterator = typename container_type::const_reverse_iterator; | ||
| using growth_trait = GrowthTrait; | ||
|
|
||
| static_assert(growth_trait::denominator() != 0, "growth_trait's denominator cannot be 0"); | ||
| static_assert(growth_trait::numerator() > growth_trait::denominator(), "growth_trait's divisor must be more than 1"); | ||
|
|
||
| constexpr tunable_vector() : _container() {} | ||
| explicit constexpr tunable_vector(allocator_type const& alloc) : _container(alloc) {} | ||
| explicit constexpr tunable_vector(size_type count, allocator_type const& alloc = allocator_type {}) | ||
| : _container(count, alloc) {} | ||
| constexpr tunable_vector(size_type count, value_type const& value, allocator_type const& alloc = allocator_type {}) | ||
| : _container(count, value, alloc) {} | ||
| template<typename InputIt> | ||
| constexpr tunable_vector(InputIt first, InputIt last, allocator_type const& alloc = allocator_type {}) | ||
| : _container(first, last, alloc) {} | ||
|
|
||
| constexpr tunable_vector(container_type const& other) : _container { other } {} | ||
| constexpr tunable_vector(container_type&& other) : _container { std::move(other) } {} | ||
|
|
||
| constexpr tunable_vector(container_type const& other, allocator_type const& alloc) : _container { other, alloc } {} | ||
| constexpr tunable_vector(container_type&& other, allocator_type const& alloc) | ||
| : _container { std::move(other), alloc } {} | ||
|
|
||
| constexpr tunable_vector(tunable_vector const& other) : _container { other._container } {} | ||
| constexpr tunable_vector(tunable_vector&& other) : _container { std::move(other._container) } {} | ||
|
|
||
| constexpr tunable_vector(tunable_vector const& other, allocator_type const& alloc) | ||
| : _container { other._container, alloc } {} | ||
| constexpr tunable_vector(tunable_vector&& other, allocator_type const& alloc) | ||
| : _container { std::move(other._container), alloc } {} | ||
|
|
||
| template<typename OtherGrowth> | ||
| constexpr tunable_vector(tunable_vector<T, OtherGrowth, Allocator> const& other) : _container { other._container } {} | ||
| template<typename OtherGrowth> | ||
| constexpr tunable_vector(tunable_vector<T, OtherGrowth, Allocator>&& other) | ||
| : _container { std::move(other._container) } {} | ||
|
|
||
| template<typename OtherGrowth> | ||
| constexpr tunable_vector(tunable_vector<T, OtherGrowth, Allocator> const& other, allocator_type const& alloc) | ||
| : _container { other._container, alloc } {} | ||
| template<typename OtherGrowth> | ||
| constexpr tunable_vector(tunable_vector<T, OtherGrowth, Allocator>&& other, allocator_type const& alloc) | ||
| : _container { std::move(other._container), alloc } {} | ||
|
|
||
| constexpr tunable_vector(std::initializer_list<value_type> init, allocator_type const& alloc = allocator_type {}) | ||
| : _container(init, alloc) {} | ||
|
|
||
| constexpr tunable_vector& operator=(container_type const& other) { | ||
| _container = other; | ||
| return *this; | ||
| } | ||
| constexpr tunable_vector& operator=(container_type&& other) { | ||
| _container = std::move(other); | ||
| return *this; | ||
| } | ||
|
|
||
| constexpr tunable_vector& operator=(tunable_vector const& other) { | ||
| _container = other._container; | ||
| return *this; | ||
| } | ||
| constexpr tunable_vector& operator=(tunable_vector&& other) { | ||
| _container = std::move(other._container); | ||
| return *this; | ||
| } | ||
|
|
||
| tunable_vector& operator=(std::initializer_list<value_type> ilist) { | ||
| _container = ilist; | ||
| return *this; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr reference operator[](const size_type pos) { | ||
| OV_HARDEN_ASSERT_ACCESS(pos, "operator[]"); | ||
| return _container[pos]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reference operator[](const size_type pos) const { | ||
| OV_HARDEN_ASSERT_ACCESS(pos, "operator[]"); | ||
| return _container[pos]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr reference front() { | ||
| OV_HARDEN_ASSERT_NONEMPTY("front"); | ||
| return _container[0]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reference front() const { | ||
| OV_HARDEN_ASSERT_NONEMPTY("front"); | ||
| return _container[0]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr reference back() { | ||
| OV_HARDEN_ASSERT_NONEMPTY("back"); | ||
| return _container[size() - 1]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reference back() const { | ||
| OV_HARDEN_ASSERT_NONEMPTY("back"); | ||
| return _container[size() - 1]; | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr value_type* data() { | ||
| return _container.data(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr value_type const* data() const { | ||
| return _container.data(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr iterator begin() { | ||
| return _container.begin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_iterator begin() const { | ||
| return _container.begin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_iterator cbegin() const { | ||
| return _container.cbegin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr iterator end() { | ||
| return _container.end(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_iterator end() const { | ||
| return _container.end(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_iterator cend() const { | ||
| return _container.cend(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr reverse_iterator rbegin() { | ||
| return _container.rbegin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reverse_iterator rbegin() const { | ||
| return _container.rbegin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reverse_iterator crbegin() const { | ||
| return _container.crbegin(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr reverse_iterator rend() { | ||
| return _container.rend(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr const_reverse_iterator rend() const { | ||
| return _container.rend(); | ||
| } | ||
|
|
||
| constexpr const_reverse_iterator crend() const { | ||
| return _container.crend(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr bool empty() const { | ||
| return _container.empty(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr size_type size() const { | ||
| return _container.size(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr size_type max_size() const { | ||
| return _container.max_size(); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr size_type capacity() const { | ||
| return _container.capacity(); | ||
| } | ||
|
|
||
| constexpr void shrink_to_fit() { | ||
| _container.shrink_to_fit(); | ||
| } | ||
|
|
||
| constexpr void clear() { | ||
| _container.clear(); | ||
| } | ||
|
|
||
| constexpr iterator erase(const_iterator pos) { | ||
| return _container.erase(pos); | ||
| } | ||
|
|
||
| constexpr iterator erase(const_iterator first, const_iterator last) { | ||
| return _container.erase(first, last); | ||
| } | ||
|
|
||
| constexpr void swap(tunable_vector& vector) { | ||
| _container.swap(vector._container); | ||
| } | ||
|
|
||
| constexpr allocator_type get_allocator() const { | ||
| return _container.get_allocator(); | ||
| } | ||
|
|
||
| constexpr void push_back(value_type const& value) { | ||
| if (size() == capacity()) { | ||
| reserve_minimum(capacity() + 1); | ||
| } | ||
| _container.push_back(value); | ||
| } | ||
|
|
||
| constexpr void push_back(value_type&& value) { | ||
| if (size() == capacity()) { | ||
| reserve_minimum(capacity() + 1); | ||
| } | ||
| _container.push_back(std::move(value)); | ||
| } | ||
|
|
||
| template<typename... Args> | ||
| constexpr reference emplace_back(Args&&... args) { | ||
| if (size() == capacity()) { | ||
| reserve_minimum(capacity() + 1); | ||
| } | ||
| return _container.emplace_back(std::forward<Args>(args)...); | ||
| } | ||
|
|
||
| template<_detail::_compatible_range<T> RangeT> | ||
| constexpr void append_range(RangeT&& range) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whilst a useful method, it is not specific to tunable_vector. If there are no uses, defer the implementation to future consumers.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. vector implements it, just not in C++20, if we used C++23, the implementation could be more minimal and efficient, but since we don't this is the closest we can get outside of rewriting our own vector implementation
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless there is a real usecase I recommend against writing polyfills or backporting features.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your use of bulk_inserter_wrapper was a real usecase, that aside we are not gonna be upgrading to C++23 probably for years, the compilers and standard library are not stable nor ready enough to put for production code, some consider C++20 to still be unstable, (thankful that's mostly modules) missing features have to be backported, there is no avoiding it. |
||
| if constexpr (std::ranges::forward_range<RangeT> || std::ranges::sized_range<RangeT>) { | ||
| reserve_minimum(std::ranges::distance(range)); | ||
| ranges::move(range, std::back_inserter(*this)); | ||
| } else { | ||
| auto first = std::ranges::begin(range); | ||
| const auto last = std::ranges::end(range); | ||
|
|
||
| for (size_type free = capacity() - size(); first != last && free != size_type {}; | ||
| std::ranges::advance(first, 1), --free) { | ||
| emplace_back(*first); | ||
| } | ||
|
|
||
| if (first == last) { | ||
| return; | ||
| } | ||
|
|
||
| tunable_vector<value_type> tmp { get_allocator() }; | ||
| for (; first != last; std::ranges::advance(first, 1)) { | ||
| tmp.emplace_back(*first); | ||
| } | ||
| std::ranges::subrange subrange(std::make_move_iterator(tmp.begin()), std::make_move_iterator(tmp.end())); | ||
| append_range(subrange); | ||
| } | ||
| } | ||
|
|
||
| constexpr void resize(size_type count) { | ||
| reserve_minimum(count); | ||
| _container.resize(count); | ||
| } | ||
|
|
||
| // Prefer reserve_minimum to be more explicit and obvious. | ||
| // This is only for making it a drop-in replacement of vector. | ||
| constexpr void reserve(size_type count) { | ||
| reserve_minimum(count); | ||
| } | ||
|
|
||
| constexpr void reserve_minimum(size_type count) { | ||
|
wvpm marked this conversation as resolved.
|
||
| if (count > capacity()) { | ||
| _container.reserve(_get_capacity_for(count)); | ||
| } | ||
| } | ||
|
|
||
| constexpr void reserve_exact(size_type count) { | ||
| _container.reserve(count); | ||
| } | ||
|
|
||
| [[nodiscard]] constexpr container_type const& container() const { | ||
| return _container; | ||
| } | ||
|
|
||
| constexpr container_type&& release() && { | ||
| return std::move(_container); | ||
| } | ||
|
|
||
| private: | ||
| container_type _container; | ||
|
|
||
| [[nodiscard]] constexpr size_type _get_capacity_for(const size_type value) const { | ||
| const size_type max = max_size(); | ||
| const size_type old_capacity = capacity(); | ||
|
|
||
| if (max <= value) { | ||
| return max; | ||
| } | ||
|
|
||
| if (value <= old_capacity) { | ||
| return value; | ||
| } | ||
|
|
||
| const size_type new_capacity = old_capacity * growth_trait::numerator() / growth_trait::denominator(); | ||
|
|
||
| // If true, new_capacity overflowed something | ||
| if (OV_unlikely(new_capacity == size_type {} || new_capacity < old_capacity || new_capacity > max)) { | ||
| return max; | ||
| } | ||
|
|
||
| if (new_capacity < growth_trait::initial_allocation_size()) { | ||
| return growth_trait::initial_allocation_size(); | ||
| } | ||
|
|
||
| // Something odd has happened | ||
| if (unlikely(new_capacity / growth_trait::numerator() < old_capacity / growth_trait::denominator())) { | ||
| return value; | ||
| } | ||
|
|
||
| if (value > new_capacity) { | ||
| return value; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems wrong.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only specifically in the case that you reserve more than the next factor size, this is what all the standard libraries will do with resize.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tunable_vector deviates from standard library implementation details on purpose.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well your first suggestion would break amortized O(1) growth completely. As for the other two, I'm not so certain of that, changing it changes the characteristics of the growth, the point of this container is to make reserve by default naturally follow the growth formula, with the growth factor being defined by the traits. I don't want to diverge far from the vector implementation beyond the growth factor behavior because I know the characteristics of vector. |
||
| } | ||
|
|
||
| return new_capacity; | ||
| } | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/OpenVicProject/OpenVic-Simulation/pull/754/changes#diff-66b217c20a8a9c4dcc694b0c9515140cafa6b47f01a4aaa7127fba1c129ef66c
Does the same. This is a common concern.
We can leave it here for now and replace it with RangeConcepts.hpp once that's merged.
Alternatively, feel free to extract it now in case we need it for other PRs as well.