Daily bit(e) of C++ | Implementing custom views
Daily bit(e) of C++ #411, Implementing a composable custom view in C++23.
C++20 introduced views as part of the ranges library. Unfortunately, the support for implementing custom views was missing until C++23.
In this short article, we will go over how to implement custom views and what you should keep in mind while implementing a view.
Compile-time composition of views
View composition is at the core of views; however, typically, when we talk about the composition of views, we mean the runtime composition.
Even in C++20, we can also compose views at compile time, as long as none of the views depends on a runtime argument.
Note that views::drop_while is one of the views that require mutability; however, this only applies to the final composition that includes a source range.
If your final composed view depends on a runtime argument, you could still use composition with a factory function returning a concrete instance of your composed view. The more principled approach is to build a custom view instead.
Building a custom view
Let’s start with a simple example of a view that exposes all elements of the underlying range. C++20 includes the std::ranges::view_interface, simplifying the job; all we have to do is implement begin() and end().
You might be tempted to implement something like the following example:
However, this implementation has a fatal flaw.
If we wrap a temporary, we end up with dangling iterators.
Sadly, this is not something we can avoid by simply forbidding construction from temporaries since that is precisely what we will encounter when we compose views.
The good thing is that the ranges library already provides the std::ranges::views::all, which correctly handles this. Before we get to that, here is one (not practical) way you solve the problem (views::all uses a more fleshed-out version of the same idea):
However, as I mentioned, the standard library already provides a views::all, which comes bundled with a helper type std::ranges::views::all_t.
When the argument is an r-value, the views::all_t will produce an owning_view, which takes ownership of the argument. If the argument is an l-value the result will be a ref_view which only references the argument.
While we no longer have issues with lifetime, we also have a fairly pointless wrapper around std::views::all that already does exactly what was our initial aim. Let’s switch gears and implement a view that does some meaningful work.
All prefixes of a range
Let’s build a view that will produce all the prefixes of a range. We can express the main idea of this view using composition and a factory function.
However, since we are working with iterators, this factory function can only operate on l-values.
We need to wrap this factory in a view to work around this limitation, using the technique from the previous section.
The ideas are the same. We have a more complex deduction guide since the second argument of the template gets its type from the result of invoking make_all_prefixes.
Finally, we need to enable composition for our view. Technically, this already works if our new view is the left argument, but to get this working completely, we need C++23 support.
C++23 introduced the std::ranges::range_adaptor_closure. The pipe composition support and the corresponding overloads are part of the standard library, so all we have to do is wrap our view into a functor that accepts a range as its first argument.
And with that, we have a fully working, composable view.