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_); } };