diff --git a/CMakeLists.txt b/CMakeLists.txt index f16b8717..4a993257 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,7 @@ set(SPARROW_HEADERS ${SPARROW_INCLUDE_DIR}/sparrow/allocator.hpp ${SPARROW_INCLUDE_DIR}/sparrow/array_data.hpp ${SPARROW_INCLUDE_DIR}/sparrow/buffer.hpp + ${SPARROW_INCLUDE_DIR}/sparrow/buffer_view.hpp ${SPARROW_INCLUDE_DIR}/sparrow/data_type.hpp ${SPARROW_INCLUDE_DIR}/sparrow/data_traits.hpp ${SPARROW_INCLUDE_DIR}/sparrow/dynamic_bitset.hpp diff --git a/include/sparrow/allocator.hpp b/include/sparrow/allocator.hpp index ae514081..3ef93af4 100644 --- a/include/sparrow/allocator.hpp +++ b/include/sparrow/allocator.hpp @@ -17,8 +17,8 @@ #include #include #include -#include #include +#include #include namespace sparrow @@ -29,16 +29,19 @@ namespace sparrow */ template concept allocator = std::copy_constructible> - and std::move_constructible> - and std::equality_comparable> - and requires(std::remove_cvref_t& alloc, - typename std::remove_cvref_t::value_type* p, - std::size_t n) - { - typename std::remove_cvref_t::value_type; - { alloc.allocate(n) } -> std::same_as::value_type*>; - { alloc.deallocate(p, n) } -> std::same_as; - }; + and std::move_constructible> + and std::equality_comparable> + and requires( + std::remove_cvref_t& alloc, + typename std::remove_cvref_t::value_type* p, + std::size_t n + ) { + typename std::remove_cvref_t::value_type; + { + alloc.allocate(n) + } -> std::same_as::value_type*>; + { alloc.deallocate(p, n) } -> std::same_as; + }; /* * When the allocator A with value_type T satisfies this concept, any_allocator @@ -46,8 +49,9 @@ namespace sparrow * (Small Buffer Optimization). */ template - concept can_any_allocator_sbo = allocator && - (std::same_as> || std::same_as>); + concept can_any_allocator_sbo = allocator + && (std::same_as> + || std::same_as>); /* * Type erasure class for allocators. This allows to use any kind of allocator @@ -71,18 +75,17 @@ namespace sparrow template any_allocator(A&& alloc) - requires (not std::same_as, any_allocator> - and sparrow::allocator) + requires(not std::same_as, any_allocator> and sparrow::allocator) : m_storage(make_storage(std::forward(alloc))) { } [[nodiscard]] T* allocate(std::size_t n); void deallocate(T* p, std::size_t n); - + any_allocator select_on_container_copy_construction() const; - bool equal(const any_allocator& rhs) const; + bool equal(const any_allocator& rhs) const; private: @@ -100,7 +103,10 @@ namespace sparrow { A m_alloc; - explicit impl(A alloc) : m_alloc(std::move(alloc)) {} + explicit impl(A alloc) + : m_alloc(std::move(alloc)) + { + } [[nodiscard]] T* allocate(std::size_t n) override { @@ -127,12 +133,8 @@ namespace sparrow } }; - using storage_type = std::variant - < - std::allocator, - std::pmr::polymorphic_allocator, - std::unique_ptr - >; + using storage_type = std:: + variant, std::pmr::polymorphic_allocator, std::unique_ptr>; template std::unique_ptr make_storage(A&& alloc) const @@ -141,35 +143,56 @@ namespace sparrow } template - requires can_any_allocator_sbo + requires can_any_allocator_sbo A&& make_storage(A&& alloc) const { return std::forward(alloc); } - template struct overloaded : Ts... { using Ts::operator()...; }; + template + struct overloaded : Ts... + { + using Ts::operator()...; + }; // Although not required in C++20, clang needs it to build the code below - template overloaded(Ts...) -> overloaded; - + template + overloaded(Ts...) -> overloaded; + storage_type copy_storage(const storage_type& rhs) const { - return std::visit(overloaded { - [](const auto& arg) -> storage_type { return { std::decay_t(arg) }; }, - [](const std::unique_ptr& arg) -> storage_type { return { arg->clone() }; } - }, rhs); + return std::visit( + overloaded{ + [](const auto& arg) -> storage_type + { + return {std::decay_t(arg)}; + }, + [](const std::unique_ptr& arg) -> storage_type + { + return {arg->clone()}; + } + }, + rhs + ); } template decltype(auto) visit_storage(F&& f) { - return std::visit([&f](auto&& arg) - { - using A = std::decay_t; - if constexpr (can_any_allocator_sbo) - return f(arg); - else - return f(*arg); - }, m_storage); + return std::visit( + [&f](auto&& arg) + { + using A = std::decay_t; + if constexpr (can_any_allocator_sbo) + { + return f(arg); + } + else + { + return f(*arg); + } + }, + m_storage + ); } storage_type m_storage; @@ -194,13 +217,23 @@ namespace sparrow template [[nodiscard]] T* any_allocator::allocate(std::size_t n) { - return visit_storage([n](auto& allocator) { return allocator.allocate(n); }); + return visit_storage( + [n](auto& allocator) + { + return allocator.allocate(n); + } + ); } template void any_allocator::deallocate(T* p, std::size_t n) { - return visit_storage([n, p](auto& allocator) { return allocator.deallocate(p, n); }); + return visit_storage( + [n, p](auto& allocator) + { + return allocator.deallocate(p, n); + } + ); } template @@ -213,33 +246,49 @@ namespace sparrow bool any_allocator::equal(const any_allocator& rhs) const { // YOLO!! - return std::visit([&rhs](auto&& arg) - { - using A = std::decay_t; - if constexpr (can_any_allocator_sbo) + return std::visit( + [&rhs](auto&& arg) { - return std::visit([&arg](auto&& arg2) + using A = std::decay_t; + if constexpr (can_any_allocator_sbo) { - using A2 = std::decay_t; - if constexpr (can_any_allocator_sbo && std::same_as) - return arg == arg2; - else - return false; - }, rhs.m_storage); - } - else - { - return std::visit([&arg](auto&& arg2) + return std::visit( + [&arg](auto&& arg2) + { + using A2 = std::decay_t; + if constexpr (can_any_allocator_sbo && std::same_as) + { + return arg == arg2; + } + else + { + return false; + } + }, + rhs.m_storage + ); + } + else { - using A2 = std::decay_t; - if constexpr (can_any_allocator_sbo) - return false; - else - return arg->equal(*arg2); - }, rhs.m_storage); - } - - }, m_storage); + return std::visit( + [&arg](auto&& arg2) + { + using A2 = std::decay_t; + if constexpr (can_any_allocator_sbo) + { + return false; + } + else + { + return arg->equal(*arg2); + } + }, + rhs.m_storage + ); + } + }, + m_storage + ); } template @@ -248,4 +297,3 @@ namespace sparrow return lhs.equal(rhs); } } - diff --git a/include/sparrow/buffer.hpp b/include/sparrow/buffer.hpp index 1b828be2..d5172b42 100644 --- a/include/sparrow/buffer.hpp +++ b/include/sparrow/buffer.hpp @@ -18,454 +18,838 @@ #include #include #include +#include +#include +#include "sparrow/allocator.hpp" #include "sparrow/iterator.hpp" #include "sparrow/mp_utils.hpp" namespace sparrow { + /** - * @class buffer_base - * @brief Base class for buffer and buffer_view - * - * This class implements the common API for buffer and buffer_view. - * The only difference between these two inheriting classes is - * that buffer owns its memory while buffer_view only references it. + * Base class for buffer. * - **/ + * This class provides memory management for the buffer class. + * The constructor and destructor perform allocation and deallocation + * of the internal storage, but do not initialize any element. + */ template class buffer_base { + protected: + + using allocator_type = any_allocator; + using alloc_traits = std::allocator_traits; + using pointer = typename alloc_traits::pointer; + using size_type = typename alloc_traits::size_type; + + struct buffer_data + { + pointer p_begin = nullptr; + pointer p_end = nullptr; + pointer p_storage_end = nullptr; + + buffer_data() = default; + constexpr buffer_data(buffer_data&&) noexcept; + constexpr buffer_data& operator=(buffer_data&&) noexcept; + }; + + buffer_base() = default; + + template + constexpr buffer_base(const A& a) noexcept; + + template + constexpr buffer_base(size_type n, const A& a = A()); + + template + constexpr buffer_base(pointer p, size_type n, const A& a = A()); + + ~buffer_base(); + + buffer_base(buffer_base&&) = default; + + template + constexpr buffer_base(buffer_base&& rhs, const A& a); + + constexpr allocator_type& get_allocator() noexcept; + constexpr const allocator_type& get_allocator() const noexcept; + constexpr buffer_data& get_data() noexcept; + constexpr const buffer_data& get_data() const noexcept; + + constexpr pointer allocate(size_type n); + constexpr void deallocate(pointer p, size_type n); + constexpr void create_storage(size_type n); + constexpr void assign_storage(pointer p, size_type n, size_type cap); + + private: + + allocator_type m_alloc; + buffer_data m_data; + }; + + /** + * Object that owns a piece of contiguous memory. + * + * This class provides an API similar to std::vector, with + * two main differences: + * - it is not templated by the allocator type, but makes use of + * any_allocator which type-erases it. + * - it can acquire ownership of an already allocated raw buffer. + */ + template + class buffer : private buffer_base + { + using base_type = buffer_base; + using alloc_traits = typename base_type::alloc_traits; + + static_assert( + std::same_as, T>, + "buffer must have a non-const, non-volatile, non-reference value_type" + ); + public: - using self_type = buffer_base; + using allocator_type = typename base_type::allocator_type; using value_type = T; - using reference = T&; - using const_reference = const T&; - using pointer = T*; - using const_pointer = const T*; - using size_type = std::size_t; - using difference_type = std::ptrdiff_t; - + using reference = value_type&; + using const_reference = const value_type&; + using pointer = typename alloc_traits::pointer; + using const_pointer = typename alloc_traits::const_pointer; + using size_type = typename alloc_traits::size_type; + using difference_type = typename alloc_traits::difference_type; using iterator = pointer_iterator; using const_iterator = pointer_iterator; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; - bool empty() const noexcept; - size_type size() const noexcept; + buffer() = default; + + template + requires(not std::same_as> and allocator) + constexpr explicit buffer(const A& a) + : base_type(a) + { + } + + template + constexpr explicit buffer(size_type n, const A& a = A()); + + template + constexpr buffer(size_type n, const value_type& v, const A& a = A()); - reference operator[](size_type); - const_reference operator[](size_type) const; + template + constexpr buffer(pointer p, size_type n, const A& a = A()); - reference front(); - const_reference front() const; + template + constexpr buffer(std::initializer_list init, const A& a = A()); + + template + constexpr buffer(It first, It last, const A& a = A()); + + ~buffer(); - reference back(); - const_reference back() const; + buffer(const buffer& rhs); + template + buffer(const buffer& rhs, const A& a); + + buffer(buffer&& rhs) = default; + + template + buffer(buffer&& rhs, const A& a); + + buffer& operator=(const buffer& rhs); + buffer& operator=(buffer&& rhs); + buffer& operator=(std::initializer_list init); + + // Element access + + constexpr reference operator[](size_type i); + constexpr const_reference operator[](size_type i) const; + + constexpr reference front(); + constexpr const_reference front() const; + + constexpr reference back(); + constexpr const_reference back() const; + + // TODO: make this non template and make a buffer_caster class template - U* data() noexcept; + constexpr U* data() noexcept; + // TODO: make this non template and make a buffer_caster class template - const U* data() const noexcept; + constexpr const U* data() const noexcept; - iterator begin(); - iterator end(); + // Iterators - const_iterator begin() const; - const_iterator end() const; - const_iterator cbegin() const; - const_iterator cend() const; + constexpr iterator begin() noexcept; + constexpr iterator end() noexcept; - reverse_iterator rbegin(); - reverse_iterator rend(); + constexpr const_iterator begin() const noexcept; + constexpr const_iterator end() const noexcept; - const_reverse_iterator rbegin() const; - const_reverse_iterator rend() const; - const_reverse_iterator crbegin() const; - const_reverse_iterator crend() const; + constexpr const_iterator cbegin() const noexcept; + constexpr const_iterator cend() const noexcept; - void swap(buffer_base& rhs) noexcept; - bool equal(const buffer_base& rhs) const; + constexpr reverse_iterator rbegin() noexcept; + constexpr reverse_iterator rend() noexcept; - protected: + constexpr const_reverse_iterator rbegin() const noexcept; + constexpr const_reverse_iterator rend() const noexcept; - buffer_base() = default; - buffer_base(pointer p, size_type n); - ~buffer_base() = default; + constexpr const_reverse_iterator crbegin() const noexcept; + constexpr const_reverse_iterator crend() const noexcept; - buffer_base(const self_type&) = default; - self_type& operator=(const self_type&) = default; + // Capacity - buffer_base(self_type&&) = default; - self_type& operator=(self_type&&) = default; + constexpr bool empty() const noexcept; + constexpr size_type capacity() const noexcept; + constexpr size_type size() const noexcept; + constexpr size_type max_size() const noexcept; + constexpr void reserve(size_type new_cap); + constexpr void shrink_to_fit(); - void reset(pointer p, size_type n); + // Modifiers + + constexpr void clear(); + constexpr void resize(size_type new_size); + constexpr void resize(size_type new_size, const value_type& value); + constexpr void swap(buffer& rhs); private: - pointer p_data = nullptr; - size_type m_size = 0u; + using base_type::get_allocator; + using base_type::get_data; + + template + constexpr void resize_impl(size_type new_size, F&& initializer); + + template + constexpr void assign_range_impl(It first, It last, std::forward_iterator_tag); + + constexpr void erase_at_end(pointer p); + + template + constexpr pointer allocate_and_copy(size_type n, It first, It last); + + // The following methods are static because: + // - they accept an allocator argument, and do not depend on + // the state of buffer + // - taking an allocator argument instead of relying on get_allocator + // will make it easier to support allocators that propagate + // on copy / move, as these methods will be called with allocators + // from different instances of buffers. + + static constexpr size_type check_init_length(size_type n, const allocator_type& a); + + static constexpr size_type max_size_impl(const allocator_type& a) noexcept; + + static constexpr pointer default_initialize(pointer begin, size_type n, allocator_type& a); + + static constexpr pointer + fill_initialize(pointer begin, size_type n, const value_type& v, allocator_type& a); + + template + static constexpr pointer copy_initialize(It first, It last, pointer begin, allocator_type& a); + + static constexpr void destroy(pointer first, pointer last, allocator_type& a); }; template - bool operator==(const buffer_base& lhs, const buffer_base& rhs); + constexpr bool operator==(const buffer& lhs, const buffer& rhs) noexcept; + + /****************************** + * buffer_base implementation * + ******************************/ - /** - * @class buffer - * @brief Object that owns a piece of contiguous memory - */ template - class buffer : public buffer_base + constexpr buffer_base::buffer_data::buffer_data(buffer_data&& rhs) noexcept + : p_begin(rhs.p_begin) + , p_end(rhs.p_end) + , p_storage_end(rhs.p_storage_end) { - public: + rhs.p_begin = nullptr; + rhs.p_end = nullptr; + rhs.p_storage_end = nullptr; + } - using base_type = buffer_base; - using value_type = typename base_type::value_type; - using pointer = typename base_type::pointer; - using size_type = typename base_type::size_type; + template + constexpr auto buffer_base::buffer_data::operator=(buffer_data&& rhs) noexcept -> buffer_data& + { + std::swap(p_begin, rhs.p_begin); + std::swap(p_end, rhs.p_end); + std::swap(p_storage_end, rhs.p_storage_end); + return *this; + } - buffer() = default; - explicit buffer(size_type size); - buffer(size_type size, value_type); - buffer(pointer data, size_type size); + template + template + constexpr buffer_base::buffer_base(const A& a) noexcept + : m_alloc(a) + { + } - ~buffer(); + template + template + constexpr buffer_base::buffer_base(size_type n, const A& a) + : m_alloc(a) + { + create_storage(n); + } - buffer(const buffer&); - buffer& operator=(const buffer&); + template + template + constexpr buffer_base::buffer_base(pointer p, size_type n, const A& a) + : m_alloc(a) + { + assign_storage(p, n, n); + } - buffer(buffer&&); - buffer& operator=(buffer&&); + template + buffer_base::~buffer_base() + { + deallocate(m_data.p_begin, (m_data.p_storage_end - m_data.p_begin)); + } - void resize(size_type new_size); - void resize(size_type new_size, value_type value); - void clear(); + template + template + constexpr buffer_base::buffer_base(buffer_base&& rhs, const A& a) + : m_alloc(a) + , m_data(std::move(rhs.m_data)) + { + } - private: + template + constexpr auto buffer_base::get_allocator() noexcept -> allocator_type& + { + return m_alloc; + } - pointer allocate(size_type size) const; - void deallocate(pointer mem) const; - }; + template + constexpr auto buffer_base::get_allocator() const noexcept -> const allocator_type& + { + return m_alloc; + } - /* - * @class buffer_view - * @brief Object that references but does not own a piece of contiguous memory - */ template - class buffer_view : public buffer_base + constexpr auto buffer_base::get_data() noexcept -> buffer_data& { - public: + return m_data; + } - using base_type = buffer_base; - using pointer = typename base_type::pointer; - using size_type = typename base_type::size_type; + template + constexpr auto buffer_base::get_data() const noexcept -> const buffer_data& + { + return m_data; + } - explicit buffer_view(buffer& buffer); - buffer_view(pointer data, size_type size); - ~buffer_view() = default; + template + constexpr auto buffer_base::allocate(size_type n) -> pointer + { + return alloc_traits::allocate(m_alloc, n); + } - buffer_view(const buffer_view&) = default; - buffer_view& operator=(const buffer_view&) = default; + template + constexpr void buffer_base::deallocate(pointer p, size_type n) + { + alloc_traits::deallocate(m_alloc, p, n); + } - buffer_view(buffer_view&&) = default; - buffer_view& operator=(buffer_view&&) = default; - }; + template + constexpr void buffer_base::create_storage(size_type n) + { + m_data.p_begin = allocate(n); + m_data.p_end = m_data.p_begin + n; + m_data.p_storage_end = m_data.p_begin + n; + } - /****************************** - * buffer_base implementation * - ******************************/ + template + constexpr void buffer_base::assign_storage(pointer p, size_type n, size_type cap) + { + assert(n <= cap); + m_data.p_begin = p; + m_data.p_end = p + n; + m_data.p_storage_end = p + cap; + } + + /************************* + * buffer implementation * + *************************/ + + template + template + constexpr buffer::buffer(size_type n, const A& a) + : base_type(check_init_length(n, a), a) + { + get_data().p_end = default_initialize(get_data().p_begin, n, get_allocator()); + } + + template + template + constexpr buffer::buffer(size_type n, const value_type& v, const A& a) + : base_type(check_init_length(n, a), a) + { + get_data().p_end = fill_initialize(get_data().p_begin, n, v, get_allocator()); + } + + template + template + constexpr buffer::buffer(pointer p, size_type n, const A& a) + : base_type(p, check_init_length(n, a), a) + { + } + + template + template + constexpr buffer::buffer(std::initializer_list init, const A& a) + : base_type(check_init_length(init.size(), a), a) + { + get_data().p_end = copy_initialize(init.begin(), init.end(), get_data().p_begin, get_allocator()); + } template - buffer_base::buffer_base(pointer p, size_type n) - : p_data(p) - , m_size(n) + template + constexpr buffer::buffer(It first, It last, const A& a) + : base_type(check_init_length(std::distance(first, last), a), a) + { + get_data().p_end = copy_initialize(first, last, get_data().p_begin, get_allocator()); + } + + template + buffer::~buffer() { + destroy(get_data().p_begin, get_data().p_end, get_allocator()); + } + + template + buffer::buffer(const buffer& rhs) + : base_type(rhs.size(), rhs.get_allocator()) + { + get_data().p_end = copy_initialize(rhs.begin(), rhs.end(), get_data().p_begin, get_allocator()); + } + + template + template + buffer::buffer(const buffer& rhs, const A& a) + : base_type(rhs.size(), a) + { + get_data().p_end = copy_initialize(rhs.begin(), rhs.end(), get_data().p_begin, get_allocator()); + } + + template + template + buffer::buffer(buffer&& rhs, const A& a) + : base_type(a) + { + if (rhs.get_allocator() == get_allocator()) + { + get_data() = std::move(rhs.m_data); + } + else if (!rhs.empty()) + { + this->create_storage(rhs.size()); + get_data().p_end = copy_initialize(rhs.begin(), rhs.end(), get_data().p_begin, get_allocator()); + rhs.clear(); + } } template - void buffer_base::reset(pointer p, size_type n) + buffer& buffer::operator=(const buffer& rhs) { - p_data = p; - m_size = n; + if (std::addressof(rhs) != this) + { + // We assume that any_allocator never propagates on assign + assign_range_impl(rhs.get_data().p_begin, rhs.get_data().p_end, std::random_access_iterator_tag()); + } + return *this; } template - bool buffer_base::empty() const noexcept + buffer& buffer::operator=(buffer&& rhs) { - return size() == size_type(0); + if (get_allocator() == rhs.get_allocator()) + { + get_data() = std::move(rhs.get_data()); + } + else + { + assign_range_impl( + std::make_move_iterator(rhs.begin()), + std::make_move_iterator(rhs.end()), + std::random_access_iterator_tag() + ); + rhs.clear(); + } + return *this; } template - auto buffer_base::size() const noexcept -> size_type + buffer& buffer::operator=(std::initializer_list init) { - return m_size; + assign_range_impl( + std::make_move_iterator(init.begin()), + std::make_move_iterator(init.end()), + std::random_access_iterator_tag() + ); + return *this; } template - auto buffer_base::operator[](size_type pos) -> reference + constexpr auto buffer::operator[](size_type i) -> reference { - assert(pos < size()); - return data()[pos]; + return get_data().p_begin[i]; } template - auto buffer_base::operator[](size_type pos) const -> const_reference + constexpr auto buffer::operator[](size_type i) const -> const_reference { - assert(pos < size()); - return data()[pos]; + return get_data().p_begin[i]; } template - auto buffer_base::front() -> reference + constexpr auto buffer::front() -> reference { - assert(!empty()); - return data()[0]; + return *(get_data().p_begin); } template - auto buffer_base::front() const -> const_reference + constexpr auto buffer::front() const -> const_reference { - assert(!empty()); - return data()[0]; + return *(get_data().p_begin); } template - auto buffer_base::back() -> reference + constexpr auto buffer::back() -> reference { - assert(!empty()); - return data()[m_size - 1]; + return *(get_data().p_end - 1); } template - auto buffer_base::back() const -> const_reference + constexpr auto buffer::back() const -> const_reference { - assert(!empty()); - return data()[m_size - 1]; + return *(get_data().p_end - 1); } template template - U* buffer_base::data() noexcept + constexpr U* buffer::data() noexcept { - return reinterpret_cast(p_data); + return reinterpret_cast(get_data().p_begin); } template template - const U* buffer_base::data() const noexcept + constexpr const U* buffer::data() const noexcept { - return reinterpret_cast(p_data); + return reinterpret_cast(get_data().p_begin); } template - auto buffer_base::begin() -> iterator + constexpr auto buffer::begin() noexcept -> iterator { - return iterator(p_data); + return make_pointer_iterator(get_data().p_begin); } template - auto buffer_base::end() -> iterator + constexpr auto buffer::end() noexcept -> iterator { - return iterator(p_data + m_size); + return make_pointer_iterator(get_data().p_end); } template - auto buffer_base::begin() const -> const_iterator + constexpr auto buffer::begin() const noexcept -> const_iterator { return cbegin(); } template - auto buffer_base::end() const -> const_iterator + constexpr auto buffer::end() const noexcept -> const_iterator { return cend(); } template - auto buffer_base::cbegin() const -> const_iterator + constexpr auto buffer::cbegin() const noexcept -> const_iterator { - return const_iterator(p_data); + return make_pointer_iterator(const_pointer(get_data().p_begin)); } template - auto buffer_base::cend() const -> const_iterator + constexpr auto buffer::cend() const noexcept -> const_iterator { - return const_iterator(p_data + m_size); + return make_pointer_iterator(const_pointer(get_data().p_end)); } template - auto buffer_base::rbegin() -> reverse_iterator + constexpr auto buffer::rbegin() noexcept -> reverse_iterator { return reverse_iterator(end()); } template - auto buffer_base::rend() -> reverse_iterator + constexpr auto buffer::rend() noexcept -> reverse_iterator { return reverse_iterator(begin()); } template - auto buffer_base::rbegin() const -> const_reverse_iterator + constexpr auto buffer::rbegin() const noexcept -> const_reverse_iterator { return crbegin(); } template - auto buffer_base::rend() const -> const_reverse_iterator + constexpr auto buffer::rend() const noexcept -> const_reverse_iterator { return crend(); } template - auto buffer_base::crbegin() const -> const_reverse_iterator + constexpr auto buffer::crbegin() const noexcept -> const_reverse_iterator { - return const_reverse_iterator(cend()); + return const_reverse_iterator(end()); } template - auto buffer_base::crend() const -> const_reverse_iterator + constexpr auto buffer::crend() const noexcept -> const_reverse_iterator { - return const_reverse_iterator(cbegin()); + return const_reverse_iterator(begin()); } template - void buffer_base::swap(buffer_base& rhs) noexcept + constexpr bool buffer::empty() const noexcept { - std::swap(p_data, rhs.p_data); - std::swap(m_size, rhs.m_size); + return get_data().p_begin == get_data().p_end; } template - bool buffer_base::equal(const buffer_base& rhs) const + constexpr auto buffer::capacity() const noexcept -> size_type { - return m_size == rhs.m_size && std::equal(p_data, p_data + m_size, rhs.p_data); + return static_cast(get_data().p_storage_end - get_data().p_begin); } template - bool operator==(const buffer_base& lhs, const buffer_base& rhs) + constexpr auto buffer::size() const noexcept -> size_type { - return lhs.equal(rhs); + return static_cast(get_data().p_end - get_data().p_begin); } - /************************* - * buffer implementation * - *************************/ + template + constexpr auto buffer::max_size() const noexcept -> size_type + { + return max_size_impl(get_allocator()); + } template - buffer::buffer(size_type size) - : base_type{allocate(size), size} + constexpr void buffer::reserve(size_type new_cap) { + if (new_cap > max_size()) + { + throw std::length_error("buffer::reserve called with new_cap > max_size()"); + } + if (new_cap > capacity()) + { + const size_type old_size = size(); + pointer tmp = allocate_and_copy( + new_cap, + std::make_move_iterator(get_data().p_begin), + std::make_move_iterator(get_data().p_end) + ); + destroy(get_data().p_begin, get_data().p_end, get_allocator()); + this->deallocate(get_data().p_begin, get_data().p_storage_end - get_data().p_begin); + this->assign_storage(tmp, old_size, new_cap); + } } template - buffer::buffer(size_type size, value_type value) - : base_type{allocate(size), size} + constexpr void buffer::shrink_to_fit() { - std::fill_n(base_type::data(), size, value); + if (capacity() != size()) + { + buffer(std::make_move_iterator(begin()), std::make_move_iterator(end()), get_allocator()).swap(*this); + } } template - buffer::buffer(pointer data, size_type size) - : base_type{data, size} + constexpr void buffer::clear() { + erase_at_end(get_data().p_begin); } template - buffer::~buffer() + constexpr void buffer::resize(size_type new_size) + { + resize_impl( + new_size, + [this](size_type nb_init) + { + get_data().p_end = default_initialize(get_data().p_end, nb_init, get_allocator()); + } + ); + } + + template + constexpr void buffer::resize(size_type new_size, const value_type& value) { - deallocate(base_type::data()); + resize_impl( + new_size, + [this, &value](size_type nb_init) + { + get_data().p_end = fill_initialize(get_data().p_end, nb_init, value, get_allocator()); + } + ); } template - buffer::buffer(const buffer& rhs) - : base_type{allocate(rhs.size()), rhs.size()} + constexpr void buffer::swap(buffer& rhs) { - std::copy(rhs.data(), rhs.data() + rhs.size(), base_type::data()); + std::swap(this->get_data(), rhs.get_data()); } template - buffer& buffer::operator=(const buffer& rhs) + template + constexpr void buffer::resize_impl(size_type new_size, F&& initializer) { - if (this != &rhs) + if (new_size > size()) { - buffer tmp(rhs); - base_type::swap(tmp); + const size_t nb_init = new_size - size(); + if (new_size <= capacity()) + { + initializer(nb_init); + } + else + { + reserve(new_size); + initializer(nb_init); + } + } + else if (new_size < size()) + { + erase_at_end(get_data().p_begin + new_size); } - return *this; } template - buffer::buffer(buffer&& rhs) - : base_type{rhs.data(), rhs.size()} + template + constexpr void buffer::assign_range_impl(It first, It last, std::forward_iterator_tag) { - rhs.reset(nullptr, 0u); + const size_type sz = size(); + const size_type len = std::distance(first, last); + if (len > capacity()) + { + check_init_length(len, get_allocator()); + pointer p = allocate_and_copy(len, first, last); + destroy(get_data().p_begin, get_data().p_end, get_allocator()); + this->deallocate(get_data().p_begin, capacity()); + this->assign_storage(p, len, len); + } + else if (sz >= len) + { + pointer p = std::copy(first, last, get_data().p_begin); + erase_at_end(p); + } + else + { + It mid = first; + std::advance(mid, sz); + std::copy(first, mid, get_data().p_begin); + get_data().p_end = copy_initialize(mid, last, get_data().p_end, get_allocator()); + } } template - buffer& buffer::operator=(buffer&& rhs) + constexpr void buffer::erase_at_end(pointer p) { - base_type::swap(rhs); - return *this; + destroy(p, get_data().p_end, get_allocator()); + get_data().p_end = p; } template - void buffer::resize(size_type n) + template + constexpr auto buffer::allocate_and_copy(size_type n, It first, It last) -> pointer { - // TODO: add capacity, resize if growing only and define a shrink_to_fit method - if (n != base_type::size()) + pointer p = this->allocate(n); + try + { + copy_initialize(first, last, p, get_allocator()); + } + catch (...) { - buffer tmp(n); - const size_type copy_size = std::min(base_type::size(), n); - std::copy(base_type::data(), base_type::data() + copy_size, tmp.data()); - base_type::swap(tmp); + this->deallocate(p, n); + throw; } + return p; } template - void buffer::resize(size_type n, value_type value) + constexpr auto buffer::check_init_length(size_type n, const allocator_type& a) -> size_type { - const size_type old_size = base_type::size(); - resize(n); - if (old_size < n) + if (n > max_size_impl(a)) { - std::fill(base_type::data() + old_size, base_type::data() + n, value); + throw std::length_error("cannot create buffer larger than max_size()"); } + return n; } template - void buffer::clear() + constexpr auto buffer::max_size_impl(const allocator_type& a) noexcept -> size_type { - resize(size_type(0)); + const size_type diff_max = std::numeric_limits::max(); + const size_type alloc_max = std::allocator_traits::max_size(a); + return (std::min)(diff_max, alloc_max); } template - auto buffer::allocate(size_type size) const -> pointer + constexpr auto buffer::default_initialize(pointer begin, size_type n, allocator_type& a) -> pointer { - return new T[size]; + pointer current = begin; + for (; n > 0; --n, ++current) + { + alloc_traits::construct(a, current); + } + return current; } template - void buffer::deallocate(pointer mem) const + constexpr auto + buffer::fill_initialize(pointer begin, size_type n, const value_type& v, allocator_type& a) -> pointer { - delete[] mem; + pointer current = begin; + for (; n > 0; --n, ++current) + { + alloc_traits::construct(a, current, v); + } + return current; } - /****************************** - * buffer_view implementation * - ******************************/ + template + template + constexpr auto buffer::copy_initialize(It first, It last, pointer begin, allocator_type& a) -> pointer + { + pointer current = begin; + for (; first != last; ++first, ++current) + { + alloc_traits::construct(a, current, *first); + } + return current; + } template - buffer_view::buffer_view(buffer& buffer) - : base_type{buffer.data(), buffer.size()} + constexpr void buffer::destroy(pointer first, pointer last, allocator_type& a) { + for (; first != last; ++first) + { + alloc_traits::destroy(a, first); + } } template - buffer_view::buffer_view(pointer data, size_type size) - : base_type{data, size} + constexpr bool operator==(const buffer& lhs, const buffer& rhs) noexcept { + return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); } } diff --git a/include/sparrow/buffer_view.hpp b/include/sparrow/buffer_view.hpp new file mode 100644 index 00000000..7c7399ee --- /dev/null +++ b/include/sparrow/buffer_view.hpp @@ -0,0 +1,270 @@ +// Copyright 2024 Man Group Operations Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "sparrow/buffer.hpp" + +namespace sparrow +{ + /* + * Non-owning view of a contiguous sequence of objects of type T. + * + * Although this class looks very similar to std::span, it provides + * methods that are missing in C++20 std::span (like cbegin / cend), + * and additional std::vector-like APIs. + */ + template + class buffer_view + { + public: + + using self_type = buffer_view; + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + using iterator = pointer_iterator; + using const_iterator = pointer_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + explicit buffer_view(buffer& buffer); + buffer_view(pointer p, size_type n); + // TODO: To be complete we also need a subrange(...) function + // and a constructor that takes a pair of iterators (they should + // be template parameters constrained by concept, not buffer_view::iterator). + // These are often used with view types (see std::span, which also provides + // first and last subview functions). + + bool empty() const noexcept; + size_type size() const noexcept; + + reference operator[](size_type); + const_reference operator[](size_type) const; + + reference front(); + const_reference front() const; + + reference back(); + const_reference back() const; + + template + U* data() noexcept; + + template + const U* data() const noexcept; + + iterator begin(); + iterator end(); + + const_iterator begin() const; + const_iterator end() const; + const_iterator cbegin() const; + const_iterator cend() const; + + reverse_iterator rbegin(); + reverse_iterator rend(); + + const_reverse_iterator rbegin() const; + const_reverse_iterator rend() const; + const_reverse_iterator crbegin() const; + const_reverse_iterator crend() const; + + void swap(buffer_view& rhs) noexcept; + + private: + + pointer p_data = nullptr; + size_type m_size = 0u; + }; + + template + bool operator==(const buffer_view& lhs, const buffer_view& rhs); + + /****************************** + * buffer_view implementation * + ******************************/ + + template + buffer_view::buffer_view(buffer& buffer) + : p_data(buffer.data()) + , m_size(buffer.size()) + { + } + + template + buffer_view::buffer_view(pointer p, size_type n) + : p_data(p) + , m_size(n) + { + } + + template + bool buffer_view::empty() const noexcept + { + return size() == size_type(0); + } + + template + auto buffer_view::size() const noexcept -> size_type + { + return m_size; + } + + template + auto buffer_view::operator[](size_type pos) -> reference + { + assert(pos < size()); + return data()[pos]; + } + + template + auto buffer_view::operator[](size_type pos) const -> const_reference + { + assert(pos < size()); + return data()[pos]; + } + + template + auto buffer_view::front() -> reference + { + assert(!empty()); + return data()[0]; + } + + template + auto buffer_view::front() const -> const_reference + { + assert(!empty()); + return data()[0]; + } + + template + auto buffer_view::back() -> reference + { + assert(!empty()); + return data()[m_size - 1]; + } + + template + auto buffer_view::back() const -> const_reference + { + assert(!empty()); + return data()[m_size - 1]; + } + + template + template + U* buffer_view::data() noexcept + { + return reinterpret_cast(p_data); + } + + template + template + const U* buffer_view::data() const noexcept + { + return reinterpret_cast(p_data); + } + + template + auto buffer_view::begin() -> iterator + { + return iterator(p_data); + } + + template + auto buffer_view::end() -> iterator + { + return iterator(p_data + m_size); + } + + template + auto buffer_view::begin() const -> const_iterator + { + return cbegin(); + } + + template + auto buffer_view::end() const -> const_iterator + { + return cend(); + } + + template + auto buffer_view::cbegin() const -> const_iterator + { + return const_iterator(p_data); + } + + template + auto buffer_view::cend() const -> const_iterator + { + return const_iterator(p_data + m_size); + } + + template + auto buffer_view::rbegin() -> reverse_iterator + { + return reverse_iterator(end()); + } + + template + auto buffer_view::rend() -> reverse_iterator + { + return reverse_iterator(begin()); + } + + template + auto buffer_view::rbegin() const -> const_reverse_iterator + { + return crbegin(); + } + + template + auto buffer_view::rend() const -> const_reverse_iterator + { + return crend(); + } + + template + auto buffer_view::crbegin() const -> const_reverse_iterator + { + return const_reverse_iterator(cend()); + } + + template + auto buffer_view::crend() const -> const_reverse_iterator + { + return const_reverse_iterator(cbegin()); + } + + template + void buffer_view::swap(buffer_view& rhs) noexcept + { + std::swap(p_data, rhs.p_data); + std::swap(m_size, rhs.m_size); + } + + template + bool operator==(const buffer_view& lhs, const buffer_view& rhs) + { + return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } +} diff --git a/include/sparrow/dynamic_bitset.hpp b/include/sparrow/dynamic_bitset.hpp index 7daff465..78de7c25 100644 --- a/include/sparrow/dynamic_bitset.hpp +++ b/include/sparrow/dynamic_bitset.hpp @@ -20,6 +20,7 @@ #include #include "sparrow/buffer.hpp" +#include "sparrow/buffer_view.hpp" #include "sparrow/mp_utils.hpp" namespace sparrow diff --git a/test/test_allocator.cpp b/test/test_allocator.cpp index 9d5ac6bc..acd0cced 100644 --- a/test/test_allocator.cpp +++ b/test/test_allocator.cpp @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "doctest/doctest.h" - #include #include #include @@ -21,6 +19,8 @@ #include "sparrow/allocator.hpp" +#include "doctest/doctest.h" + TEST_SUITE("any_allocator") { TEST_CASE_TEMPLATE_DEFINE("value semantic", A, value_semantic_id) @@ -81,11 +81,13 @@ TEST_SUITE("any_allocator") #if __APPLE__ // /usr/lib/libc++.1.dylib is missing the symbol __ZNSt3__13pmr20get_default_resourceEv, leading // to an exception at runtime. - TEST_CASE_TEMPLATE_INVOKE(value_semantic_id, std::allocator/*, std::pmr::polymorphic_allocator*/); - TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator/*, std::pmr::polymorphic_allocator*/); + TEST_CASE_TEMPLATE_INVOKE( + value_semantic_id, + std::allocator /*, std::pmr::polymorphic_allocator*/ + ); + TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator /*, std::pmr::polymorphic_allocator*/); #else TEST_CASE_TEMPLATE_INVOKE(value_semantic_id, std::allocator, std::pmr::polymorphic_allocator); TEST_CASE_TEMPLATE_INVOKE(allocate_id, std::allocator, std::pmr::polymorphic_allocator); #endif } - diff --git a/test/test_buffer.cpp b/test/test_buffer.cpp index 298cd6f4..f5f220f1 100644 --- a/test/test_buffer.cpp +++ b/test/test_buffer.cpp @@ -12,9 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include +#include #include "sparrow/buffer.hpp" +#include "sparrow/buffer_view.hpp" #include "doctest/doctest.h" @@ -69,6 +72,20 @@ namespace sparrow { CHECK_EQ(b3[i], expected_value); } + + buffer_test_type b4 = {1u, 3u, 5u}; + CHECK_EQ(b4.size(), 3u); + CHECK_EQ(b4[0], 1u); + CHECK_EQ(b4[1], 3u); + CHECK_EQ(b4[2], 5u); + + std::vector exp = {2u, 4u, 6u}; + buffer_test_type b5(exp.cbegin(), exp.cend()); + CHECK_EQ(b5.size(), exp.size()); + for (size_t i = 0; i < 3u; ++i) + { + CHECK_EQ(b5[i], exp[i]); + } } TEST_CASE("copy semantic") @@ -104,15 +121,7 @@ namespace sparrow CHECK_EQ(b4, control); } - TEST_CASE("empty") - { - buffer_test_type b1; - CHECK(b1.empty()); - - const std::size_t size = 4u; - buffer_test_type b2(make_test_buffer(size), size); - CHECK(!b2.empty()); - } + // Element access TEST_CASE("operator[]") { @@ -161,32 +170,124 @@ namespace sparrow CHECK_EQ(b3.data()[idx], expected_value); } - TEST_CASE("equality comparison") + // Iterators + + static_assert(std::ranges::contiguous_range); + static_assert(std::contiguous_iterator); + static_assert(std::contiguous_iterator); + + TEST_CASE("iterator") + { + const std::size_t size = 8u; + buffer_test_type b(make_test_buffer(size), size); + auto iter = b.begin(); + auto citer = b.cbegin(); + for (std::size_t i = 0; i < b.size(); ++i) + { + CHECK_EQ(*iter++, b[i]); + CHECK_EQ(*citer++, b[i]); + } + CHECK_EQ(iter, b.end()); + CHECK_EQ(citer, b.cend()); + } + + TEST_CASE("reverse_iterator") { + const std::size_t size = 8u; + buffer_test_type b(make_test_buffer(size), size); + auto iter = b.rbegin(); + auto citer = b.crbegin(); + for (std::size_t i = b.size(); i != 0u; --i) + { + CHECK_EQ(*iter++, b[i - 1]); + CHECK_EQ(*citer++, b[i - 1]); + } + CHECK_EQ(iter, b.rend()); + CHECK_EQ(citer, b.crend()); + } + + TEST_CASE("iterator_consistency") + { + const std::size_t size = 8u; + buffer_test_type b(make_test_buffer(size), size); + + { + auto iter = --b.end(); + auto riter = b.rbegin(); + auto citer = --b.cend(); + auto criter = b.crbegin(); + + while (iter != b.begin()) + { + CHECK_EQ(*iter, *riter); + CHECK_EQ(*citer, *criter); + --iter, --citer, ++riter, ++criter; + } + } + + { + auto iter = b.begin(); + auto riter = --b.rend(); + auto citer = b.cbegin(); + auto criter = --b.crend(); + + while (iter != b.end()) + { + CHECK_EQ(*iter, *riter); + CHECK_EQ(*citer, *criter); + ++iter, ++citer, --riter, --criter; + } + } + } + + // capacity + + TEST_CASE("empty") + { + buffer_test_type b1; + CHECK(b1.empty()); + const std::size_t size = 4u; - buffer_test_type b1(make_test_buffer(size), size); buffer_test_type b2(make_test_buffer(size), size); - CHECK(b1 == b2); + CHECK(!b2.empty()); + } - const std::size_t size2 = 8u; - buffer_test_type b3(make_test_buffer(size2), size2); - CHECK(b1 != b3); + TEST_CASE("capacity") + { + const std::size_t size = 4u; + buffer_test_type b(make_test_buffer(size), size); + CHECK_EQ(b.capacity(), size); } - TEST_CASE("swap") + TEST_CASE("size") + { + const std::size_t size = 4u; + buffer_test_type b(make_test_buffer(size), size); + CHECK_EQ(b.size(), size); + } + + // modifiers + + TEST_CASE("clear") { const std::size_t size1 = 4u; - const std::size_t size2 = 8u; + buffer_test_type b(make_test_buffer(size1), size1); + b.clear(); + CHECK_EQ(b.size(), 0u); + } + TEST_CASE("reserve") + { + const std::size_t size1 = 4u; buffer_test_type b1(make_test_buffer(size1), size1); - buffer_test_type b2(make_test_buffer(size2), size2); - auto* data1 = b1.data(); - auto* data2 = b2.data(); - b1.swap(b2); - CHECK_EQ(b1.size(), size2); - CHECK_EQ(b1.data(), data2); - CHECK_EQ(b2.size(), size1); - CHECK_EQ(b2.data(), data1); + buffer_test_type b2(b1); + const std::size_t new_cap = 8u; + b1.reserve(new_cap); + CHECK_EQ(b1.capacity(), new_cap); + for (std::size_t i = 0; i < size1; ++i) + { + CHECK_EQ(b1.data()[i], b2.data()[i]); + } } TEST_CASE("resize") @@ -203,47 +304,51 @@ namespace sparrow CHECK_EQ(b.data()[2], 2); const std::size_t size3 = 6u; - b.resize(size3); + const buffer_test_type::value_type v = 7u; + b.resize(size3, v); CHECK_EQ(b.size(), size3); CHECK_EQ(b.data()[2], 2); + CHECK_EQ(b.data()[4], v); + CHECK_EQ(b.data()[5], v); } - TEST_CASE("clear") + TEST_CASE("shrink_to_fit") { const std::size_t size1 = 4u; - buffer_test_type b(make_test_buffer(size1), size1); - b.clear(); - CHECK_EQ(b.size(), 0u); + buffer_test_type b1(make_test_buffer(size1), size1); + const std::size_t new_cap = 8u; + b1.reserve(new_cap); + CHECK_EQ(b1.capacity(), new_cap); + b1.shrink_to_fit(); + CHECK_EQ(b1.capacity(), size1); } - TEST_CASE("iterator") + TEST_CASE("swap") { - const std::size_t size = 8u; - buffer_test_type b(make_test_buffer(size), size); - auto iter = b.begin(); - auto citer = b.cbegin(); - for (std::size_t i = 0; i < b.size(); ++i) - { - CHECK_EQ(*iter++, b[i]); - CHECK_EQ(*citer++, b[i]); - } - CHECK_EQ(iter, b.end()); - CHECK_EQ(citer, b.cend()); + const std::size_t size1 = 4u; + const std::size_t size2 = 8u; + + buffer_test_type b1(make_test_buffer(size1), size1); + buffer_test_type b2(make_test_buffer(size2), size2); + auto* data1 = b1.data(); + auto* data2 = b2.data(); + b1.swap(b2); + CHECK_EQ(b1.size(), size2); + CHECK_EQ(b1.data(), data2); + CHECK_EQ(b2.size(), size1); + CHECK_EQ(b2.data(), data1); } - TEST_CASE("reverse_iterator") + TEST_CASE("equality comparison") { - const std::size_t size = 8u; - buffer_test_type b(make_test_buffer(size), size); - auto iter = b.rbegin(); - auto citer = b.crbegin(); - for (std::size_t i = b.size(); i != 0u; --i) - { - CHECK_EQ(*iter++, b[i - 1]); - CHECK_EQ(*citer++, b[i - 1]); - } - CHECK_EQ(iter, b.rend()); - CHECK_EQ(citer, b.crend()); + const std::size_t size = 4u; + buffer_test_type b1(make_test_buffer(size), size); + buffer_test_type b2(make_test_buffer(size), size); + CHECK(b1 == b2); + + const std::size_t size2 = 8u; + buffer_test_type b3(make_test_buffer(size2), size2); + CHECK(b1 != b3); } }