Creating a custom range in C++ (incomplete)
In this section, we will be exploring how we can create a custom view, including its corresponding range adaptor. For the sake of simplicity, we will (re)implement take_view, which is also present in the STL.
Our goal is that the following code compiles and gives the correct result, as it would with std::views::take:
const std::vector<int> n{2, 3, 5, 6, 7, 8, 9};
auto v = n | rv::filter(is_even) | views::custom_take(2);
std::ranges::copy(v,
std::ostream_iterator<int>(std::cout, " "));
Implementing the view
The first step is to implement the actual view to a range. In this view, we need to store the range and the number of elements this view should process from the range. While implementing this, we apply the range concepts available in C++20. The listing below shows an implementation, which we will walk through next.
template<std::ranges::view R>// #A Using ranges::view concept
class custom_take_view
: public std::ranges::view_interface<custom_take_view<R>> {
// #B Necessary data members:
R base_{};
std::ranges::range_difference_t<R> count_{};
public:
// #C Default constructible:
custom_take_view() = default;
// #D Constructor for range and count:
constexpr custom_take_view(
R base,
std::ranges::range_difference_t<R> count)
:base_{std::move(base)} ,count_{count}
{}
// #E view_interface members:
constexpr R base() const & {return base_;}
constexpr R base()&& {return std::move(base_);}
// #F Actual begin and end:
constexpr auto begin() {return std::ranges::begin(base_);}
constexpr auto end() {
return std::ranges::next(std::ranges::begin(base_),count_);
}
};
template<std::ranges::range R>// #G Deduction guide
custom_take_view(R&& base, std::ranges::range_difference_t<R>)
->custom_take_view<std::ranges::views::all_t<R>>;
In the template head of the class template custom_take_view, we directly start applying concepts #A. This class should work only with a view type. This concept checks that R is a range that is movable, default initializable, and is a view. The last concept checks whether R is derived from view_base. Our custom_take_view derives from view_base with the help of view_interface, using Curiously Recurring Template Pattern (CRTP).
After the head, we declare the required data members #B. In #C, we ensure that custom_take_view is default-constructible. The next constructor in #D is used when our view is created from a range and a count. This is the second item in the list in ยง3.
The view_interface requires a couple of members, which are implemented in #E. The actual implementation for begin and end is presented in #F. Here, we use the ranges begin version. In end you can see the actual implementation, the only part here that does something other than setting defaults. Using std::ranges::next, we retrieve the next iterator element after begin with an offset of count. With that, we already have a working implementation of custom_take_view.
Please note that this is a simplified version. The STL has to deal with different range types, depending on whether R is a simple_view or a sized_range. We leave this out here.
What is also needed is the Class Template Argument Deduction (CTAD) deduction guide in #G. Without this deduction guide, we cannot create a custom_take_view just by passing a range and a count.
We now have the base for our custom_take_view. Let's make our view fit nicely into all the other ranges and add the missing range adaptor.
A range adaptor for custom_take_view
The next thing we need for custom_take_view is a range adaptor. The code shown below should, in practice, be wrapped in a dedicated namespace like details.
template<std::integral T> // #A Only integrals
struct custom_take_range_adaptor_closure {
T count_;// #B Store the count
constexpr custom_take_range_adaptor_closure(T count)
: count_{count}
{}
// #C Allow it to be called with a range:
template<std::ranges::viewable_range R>
constexpr auto operator()(R&&r)const {
return custom_take_view(std::forward<R>(r),count_);
}
};