From 0b9fdf5d737112310b7e9d903c8ef69d5cfe2a09 Mon Sep 17 00:00:00 2001 From: Nahuel Espinosa Date: Mon, 7 Oct 2024 14:50:21 -0300 Subject: [PATCH] Rewrite take_evenly view Avoids using a range pipeline to implement the take_evenly view which is not great for compile times. This implementation also makes it a random access view when the underlying range is random access. Signed-off-by: Nahuel Espinosa --- beluga/include/beluga/views/take_evenly.hpp | 174 +++++++++++++----- beluga/test/beluga/views/test_take_evenly.cpp | 108 ++++++++--- 2 files changed, 215 insertions(+), 67 deletions(-) diff --git a/beluga/include/beluga/views/take_evenly.hpp b/beluga/include/beluga/views/take_evenly.hpp index 86de51ae0..038e27bab 100644 --- a/beluga/include/beluga/views/take_evenly.hpp +++ b/beluga/include/beluga/views/take_evenly.hpp @@ -30,64 +30,146 @@ namespace beluga::views { namespace detail { -/// Implementation detail for a take_evenly range adaptor object. -struct take_evenly_fn { // NOLINT(readability-identifier-naming) - /// Overload that implements the take_evenly algorithm. - /** - * \tparam Range A [sized range](https://en.cppreference.com/w/cpp/ranges/sized_range). - * \param range Source range from where to take elements. - * \param count Number of elements to take. - * - * If `count` or the range size are zero, it returns an empty range. - * If `count` is greater than the range size, it returns all the elements. - * The first and last elements of the range are always included. - */ - template - constexpr auto operator()(Range&& range, std::size_t count) const { - const std::size_t size = ranges::size(range); +/// \cond detail - const auto filter_function = [size, count](const auto& pair) { - if ((size == 0UL) || (count == 0UL)) { - return false; - } +template +struct take_evenly_view : public ranges::view_facade, ranges::finite> { + public: + static_assert(ranges::view_); + static_assert(ranges::forward_range); + static_assert(ranges::sized_range); - if (count > size) { - return true; - } + constexpr take_evenly_view() = default; - const auto [index, _] = pair; - if (count == 1UL) { - return index == 0UL; - } + constexpr take_evenly_view(Range range, std::size_t count) + : range_{std::move(range)}, count_{count}, size_{ranges::size(range_)} {} + + [[nodiscard]] constexpr std::size_t size() const { + if (size_ == 0U) { + return 0U; + } + + if (count_ > size_) { + return size_; + } + + return count_; + } + + private: + friend ranges::range_access; - if ((index == 0UL) || (index == size - 1UL)) { - return true; + struct cursor { + public: + cursor() = default; + + constexpr cursor(const take_evenly_view* view, std::size_t pos) noexcept : view_{view}, pos_{pos} { + if (pos < view_->count_) { + it_ = ranges::begin(view_->range_); + ranges::advance(it_, view_->compute_offset(pos)); } + } + + [[nodiscard]] constexpr decltype(auto) read() const { return *it_; } + + [[nodiscard]] constexpr bool equal(const cursor& other) const { return view_ == other.view_ && pos_ == other.pos_; } + + [[nodiscard]] constexpr bool equal(const ranges::default_sentinel_t&) const { + return pos_ >= view_->count_ || pos_ >= view_->size_; + } + + constexpr void next() { + const std::size_t new_pos = pos_ + 1; + ranges::advance(it_, view_->compute_offset(pos_, new_pos)); + pos_ = new_pos; + } + + template < + class T = Range, + std::enable_if_t && ranges::bidirectional_range, int> = 0> + constexpr void prev() { + const std::size_t new_pos = pos_ - 1; + ranges::advance(it_, view_->compute_offset(pos_, new_pos)); + pos_ = new_pos; + } + + template < + class T = Range, + std::enable_if_t && ranges::random_access_range, int> = 0> + constexpr void advance(std::ptrdiff_t distance) { + const auto new_pos = static_cast(static_cast(pos_) + distance); + ranges::advance(it_, view_->compute_offset(pos_, new_pos)); + pos_ = new_pos; + } + + template < + class T = Range, + std::enable_if_t && ranges::random_access_range, int> = 0> + [[nodiscard]] constexpr std::ptrdiff_t distance_to(const cursor& other) const { + return static_cast(other.pos_) - static_cast(pos_); + } + + template < + class T = Range, + std::enable_if_t && ranges::random_access_range, int> = 0> + [[nodiscard]] constexpr std::ptrdiff_t distance_to(const ranges::default_sentinel_t&) const { + return static_cast(view_->count_) - static_cast(pos_); + } + + private: + const take_evenly_view* view_; + std::size_t pos_; + + ranges::iterator_t it_; + }; + + [[nodiscard]] constexpr auto begin_cursor() const { return cursor{this, 0U}; } + + [[nodiscard]] constexpr auto end_cursor() const noexcept { return ranges::default_sentinel_t{}; } + + [[nodiscard]] constexpr std::ptrdiff_t compute_offset(std::size_t pos) const { + if (pos == 0U) { + return 0; + } + + if (count_ == 1U) { + return static_cast(size_); + } + + const std::ptrdiff_t a = static_cast(pos) * (static_cast(size_) - 1); + const std::ptrdiff_t b = static_cast(count_) - 1; + return a / b + ((a % b == 0) ? 0 : 1); + } + + [[nodiscard]] constexpr std::ptrdiff_t compute_offset(std::size_t current, std::size_t target) const { + if (count_ > size_) { + return static_cast(target) - static_cast(current); + } + + return compute_offset(target) - compute_offset(current); + } - const std::size_t m0 = (index - 1UL) * (count - 1UL) / (size - 1UL); - const std::size_t m1 = index * (count - 1UL) / (size - 1UL); - return m0 != m1; - }; + Range range_; + std::size_t count_; + std::size_t size_; +}; + +template +take_evenly_view(Range&& range, std::size_t max) -> take_evenly_view>; - // `cache1` ensures that views prior to `filter` in the pipeline are iterated exactly once. - // This is needed because filter needs to dereference the input iterator twice. - return ranges::views::enumerate(range) | ranges::views::cache1 | ranges::views::filter(filter_function) | - beluga::views::elements<1>; +struct take_evenly_fn { + template + constexpr auto operator()(Range&& range, std::size_t count) const { + return take_evenly_view{std::forward(range), count}; } - /// Overload that returns a view closure to compose with other views. - /** - * \param count Number of elements to take. - * - * If `count` or the range size are zero, it returns an empty range. - * If `count` is greater than the range size, it returns all the elements. - * The first and last elements of the range are always included. - */ constexpr auto operator()(std::size_t count) const { return ranges::make_view_closure(ranges::bind_back(take_evenly_fn{}, count)); } }; +/// \endcond + } // namespace detail /// [Range adaptor object](https://en.cppreference.com/w/cpp/named_req/RangeAdaptorObject) that @@ -97,9 +179,9 @@ struct take_evenly_fn { // NOLINT(readability-identifier-naming) * elements evenly spaced over the source range. * If `count` or the range size are zero, it returns an empty range. * If `count` is greater than the range size, it returns all the elements. - * The first and last elements of the range are always included. + * The first element of the range is always included. */ -inline constexpr detail::take_evenly_fn take_evenly; // NOLINT(readability-identifier-naming) +inline constexpr detail::take_evenly_fn take_evenly; } // namespace beluga::views diff --git a/beluga/test/beluga/views/test_take_evenly.cpp b/beluga/test/beluga/views/test_take_evenly.cpp index fb2ecfbd0..7e5cb3072 100644 --- a/beluga/test/beluga/views/test_take_evenly.cpp +++ b/beluga/test/beluga/views/test_take_evenly.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -24,75 +25,140 @@ namespace { +TEST(TakeEvenlyView, ConceptChecksFromContiguousRange) { + auto input = std::array{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto output = beluga::views::take_evenly(input, 2); + + static_assert(ranges::common_range); + static_assert(!ranges::common_range); + + static_assert(!ranges::viewable_range); + static_assert(ranges::viewable_range); + + static_assert(ranges::forward_range); + static_assert(ranges::forward_range); + + static_assert(ranges::sized_range); + static_assert(ranges::sized_range); + + static_assert(ranges::bidirectional_range); + static_assert(ranges::bidirectional_range); + + static_assert(ranges::random_access_range); + static_assert(ranges::random_access_range); + + static_assert(ranges::contiguous_range); + static_assert(!ranges::contiguous_range); + + static_assert(ranges::range); + static_assert(ranges::semiregular); + static_assert(ranges::enable_view); +} + +TEST(TakeEvenlyView, ConceptChecksFromBidirectionalRange) { + auto input = std::list{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto output = beluga::views::take_evenly(input, 2); + + static_assert(ranges::forward_range); + static_assert(ranges::forward_range); + + static_assert(ranges::bidirectional_range); + static_assert(ranges::bidirectional_range); + + static_assert(!ranges::random_access_range); + static_assert(!ranges::random_access_range); +} + TEST(TakeEvenlyView, NoElementsTakeZero) { const auto input = std::vector{}; - const auto output = input | beluga::views::take_evenly(0) | ranges::to; + auto output = input | beluga::views::take_evenly(0); ASSERT_EQ(output.size(), 0); + ASSERT_EQ(ranges::to(output).size(), 0); } TEST(TakeEvenlyView, NoElements) { const auto input = std::vector{}; - const auto output = input | beluga::views::take_evenly(1) | ranges::to; + auto output = input | beluga::views::take_evenly(1); ASSERT_EQ(output.size(), 0); + ASSERT_EQ(ranges::to(output).size(), 0); } TEST(TakeEvenlyView, TakeZero) { const auto input = std::vector{1, 2, 3, 4}; - const auto output = input | beluga::views::take_evenly(0) | ranges::to; + auto output = input | beluga::views::take_evenly(0); ASSERT_EQ(output.size(), 0); + ASSERT_EQ(ranges::to(output).size(), 0); } TEST(TakeEvenlyView, TakeOne) { const auto input = std::vector{1, 2, 3, 4}; - const auto output = input | beluga::views::take_evenly(1) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1)); + auto output = input | beluga::views::take_evenly(1); + ASSERT_EQ(output.size(), 1); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1)); } TEST(TakeEvenlyView, TakeAll) { const auto input = std::vector{1, 2, 3, 4}; - const auto output = input | beluga::views::take_evenly(10) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 2, 3, 4)); + auto output = input | beluga::views::take_evenly(10); + ASSERT_EQ(output.size(), 4); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 2, 3, 4)); } TEST(TakeEvenlyView, TakeTwoFromFour) { const auto input = std::vector{1, 2, 3, 4}; - const auto output = input | beluga::views::take_evenly(2) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 4)); + auto output = input | beluga::views::take_evenly(2); + ASSERT_EQ(output.size(), 2); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 4)); } TEST(TakeEvenlyView, TakeThreeFromFive) { const auto input = std::vector{1, 2, 3, 4, 5}; - const auto output = input | beluga::views::take_evenly(3) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 3, 5)); + auto output = input | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 3, 5)); } TEST(TakeEvenlyView, TakeThreeFromSix) { const auto input = std::vector{1, 2, 3, 4, 5, 6}; - const auto output = input | beluga::views::take_evenly(3) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 4, 6)); + auto output = input | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 4, 6)); } TEST(TakeEvenlyView, TakeThreeFromNine) { const auto input = std::vector{1, 2, 3, 4, 5, 6, 7, 8, 9}; - const auto output = input | beluga::views::take_evenly(3) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 5, 9)); + auto output = input | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 5, 9)); } TEST(TakeEvenlyView, TakeThreeFromFour) { const auto input = std::vector{1, 2, 3, 4}; - const auto output = input | beluga::views::take_evenly(3) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 3, 4)); + auto output = input | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 3, 4)); } TEST(TakeEvenlyView, TakeSixFromTen) { const auto input = std::vector{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - const auto output = input | beluga::views::take_evenly(6) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 3, 5, 7, 9, 10)); + auto output = input | beluga::views::take_evenly(6); + ASSERT_EQ(output.size(), 6); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 3, 5, 7, 9, 10)); } TEST(TakeEvenlyView, TakeFromGenerator) { - const auto output = ranges::views::iota(1, 6) | beluga::views::take_evenly(3) | ranges::to; - ASSERT_THAT(output, testing::ElementsAre(1, 3, 5)); + auto output = ranges::views::iota(1, 6) | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_THAT(ranges::to(output), testing::ElementsAre(1, 3, 5)); +} + +TEST(TakeEvenlyView, RandomAccess) { + const auto input = std::vector{1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto output = input | beluga::views::take_evenly(3); + ASSERT_EQ(output.size(), 3); + ASSERT_EQ(output[0], 1); + ASSERT_EQ(output[1], 5); + ASSERT_EQ(output[2], 9); } } // namespace