...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
C++20 introduced views and view adaptors. The adaptors have a nice "pipe" syntax, like:
for (auto x : range | std::views::reverse | std::views::take(10)) { // Here, we get the last 10 values of range, in reverse order. }
However, the C++20 standard provides you no help in making your own views and
view adaptors that work this way. C++23 introduces std::range_adaptor_closure
,
a base type that allows your view adaptors and std
view adaptors to interoperate in pipe expressions.
Boost.STLInterfaces provides a type boost::stl_interfaces::range_adaptor_closure
that is defined to be a work-alike of std::range_adaptor_closure
in pre-C++23 builds, and is an alias for std::range_adaptor_closure
in C++23 and later.
Note | |
---|---|
For GCC 12, it was necessary to make |
all()
Let's take a look at a very simple view adaptor, all()
. all()
is a simplified version of std::views::all()
that only works with lvalues.
First, we need a small amount of C++20 backported for use in earlier build modes:
// These act as stand-ins for std::ranges::iterator_t and -sentinel_t; we // need these in C++17 and earlier. They are not really complete, because // they ignore free implementations of begin() and end(). This means they // don't support builtin arrays, or other ranges that have ADL-findable // begin() and end(). If you make your own versions, you should probably // use the boost::begin() and -end() from Boost.Range, or the // implementations from range-v3. template<typename T> using iterator_t = decltype(std::declval<T &>().begin()); template<typename T> using sentinel_t = decltype(std::declval<T &>().end());
Here is the view type itself:
// This type allows us to implement a simplified version of the // std::views::all range adaptor for pre-C++20 builds. Instead of // producing different kinds of ranges based on whether R is a // std::ranges::view, or would be better represented as a // std::ranges::ref_view or std::ranges::owning_view, it just grabs // begin() and end() out of R. It also uses member-begin() and -end(), so // it doesn't work with builtin arrays. It should probably use // boost::begin() and -end(), or something comparable. // // We constrain the template to only accept object-types, so that we don't // instantiate all_view with pointer or reference types. We should also // require that R have .begin() and .end() members, but this is an // intentionally simplified example. // // We're putting the view in a detail namespace, because we don't expect // users to use our view directly; they should use the associated view // adaptor instead. If you also want users to directly construct your // view-type, you would move it out of detail::. // // If you want to make views and view adaptors that will work with // pre-C++20 code, and then provide concept constraints in C++20 and // later, this is a reasonable pattern -- write the template-head twice: // once for C++20 concepts, and one for SFINAE. Note that // BOOST_STL_INTERFACES_USE_CONCEPTS includes defined(__cpp_lib_concepts) // && defined(__cpp_lib_ranges), and any preprocessor predicate you use // should as well. #if BOOST_STL_INTERFACES_USE_CONCEPTS template<typename R> requires std::is_object_v<R> #else template< typename R, typename Enable = std::enable_if_t<std::is_object<R>::value>> #endif struct all_view : boost::stl_interfaces::view_interface<all_view<R>> { using iterator = iterator_t<R>; using sentinel = sentinel_t<R>; // Here, we want a constructor that takes a forwarding reference, so // we introduce a new template parameter R2, and constrain it to be // the same as R. The int parameter is there to prevent getting in // the way of the special member functions like the copy constructor. // Since we don't want users directly constructing this type anyway, // the non-ideal ergonomics of this extra int don't matter. #if BOOST_STL_INTERFACES_USE_CONCEPTS template<typename R2> requires std::is_same_v<std::remove_reference_t<R2>, R> #else template< typename R2, typename E = std::enable_if_t< std::is_same<std::remove_reference_t<R2>, R>::value>> #endif explicit all_view(int, R2 && r) : first_(r.begin()), last_(r.end()) {} iterator begin() const { return first_; } sentinel end() const { return last_; } private: iterator first_; sentinel last_; }; // This just makes out implementations below a bit easier to write. #if defined(__cpp_deduction_guides) template<typename R> all_view(int, R &&)->detail::all_view<std::remove_reference_t<R>>; #endif
If we want to make our view adaptor in the style of the standard library, we
need an impl-struct
:
// For C++20 views, there is usually some type like this. This type // implements the functions that construct our view. An invocable object // that uses this implementation will follow. We need to inherit from // range_adaptor_closure to make our view adaptor compatible with other // view adaptors using the operator| "pipe" syntax. struct all_impl : boost::stl_interfaces::range_adaptor_closure<all_impl> { template<typename R> constexpr auto operator()(R && r) const { // The use of std::remove_reference is important, so that we // instantiate all_view with a non-reference type. It is also // important not to use std::decay or std::remove_cvref here // instead. If you do that and you pass a T const & r, you'll end // up trying to initialize all_view<T>::first_ (which is a // T::iterator) from r.begin(), which is a T::const_iterator. // That won't work. return all_view<std::remove_reference_t<R>>(0, (R &&) r); } };
... followed by an invocable object:
// Here we create the actual invocable that the user will call. It is just a // constexpr all_impl variable. Before C++17, you need to put it in an // anonymous namespace to avoid violating the ODR. With this in scope, the // user has everything necessary to use old_all(). I called it old_all(), // because there's an even easier way to do this, as shown with all() below. #if defined(__cpp_inline_variables) inline constexpr detail::all_impl old_all; #else namespace { constexpr detail::all_impl old_all; } #endif
With these definitions, we can use old_all()
with the standard range adaptors:
for (auto x : old_all(range) | std::views::reverse) { // etc. }
However, Boost.STLInterfaces provides a simpler facility, based on the example in P2387:
// This is the preferred way to make a view adaptor. We can use a simple // template called closure that already inherits from // boost::stl_interfaces::range_adaptor_closure, and takes any function that // can construct a closure from a given range. In this case, our closure is // just an all_view. Later we'll see other kinds of closures. inline constexpr boost::stl_interfaces::closure all = []<typename R>(R && r) { return detail::all_view(0, (R &&) r); };
This does everything that all_impl
and old_all
(combined) do above,
but with a lot less typing. From here on, we'll use boost::stl_interfaces::closure
instead of the more verbose technique.
Finally, we should make sure all_view
is treated as a borrowed range:
// Any view that you make that is a proper view -- that is, it does not own // the elements between its .begin() and .end() -- should be designated as a // borrowed range, so that the std::ranges code treats is properly. Without // this, std::ranges code will assume that .begin() taken from an rvalue // reference to your view type is a dangling iterator. // // As an example of this behavior, say you call // std::ranges::find(std::vector<int>{}, 42). The result will be a // std::dangling instead of an iterator, because any iterator you pull out of // a std::vector<int> && is potentially a dangling reference to an element of // a temporary std::vector<int>. #if BOOST_STL_INTERFACES_USE_CONCEPTS namespace std::ranges { template<typename View> inline constexpr bool enable_borrowed_range<detail::all_view<View>> = true; } #endif
reverse()
reverse()
is more interesting than all()
, in that it does something more useful than
simple adaptation. It is still very simple; all it does is provide a reversed
view of a given view. Here is its definition:
// We need to treat iterator/sentinel ranges differently from iterator // ranges (a.k.a. common_ranges). If the iterator and sentinel are // different types, we need to advance the iterator to the end of the // range before we can move through the range in reverse. template<bool CommonRange> struct set_rev_rng_first { template<typename V> static auto call(V const & v) { return boost::stl_interfaces::make_reverse_iterator(v.end()); } }; template<> struct set_rev_rng_first<false> { template<typename V> static auto call(V const & v) { auto v_f = v.begin(); auto const v_l = v.end(); while (v_f != v_l) { ++v_f; } return boost::stl_interfaces::make_reverse_iterator(v_f); } }; // This view reverses whatever view you construct it from. Unlike // all_view, it requires that it be constructed from a view. This is // enforced through a constraint in C++20 and later, but is left up to the // user in earlier C++ modes. #if BOOST_STL_INTERFACES_USE_CONCEPTS template<std::ranges::view View> requires std::is_object_v<View> #else template< typename View, typename Enable = std::enable_if_t<std::is_object<View>::value>> #endif struct reverse_view : boost::stl_interfaces::view_interface<reverse_view<View>> { using view_iterator = iterator_t<View>; using view_sentinel = sentinel_t<View>; // This would be better off as a constraint in C++20 and later. static_assert( std::is_base_of< std::bidirectional_iterator_tag, typename std::iterator_traits< view_iterator>::iterator_category>::value, "A reversed view must have bidirectional iterators."); using iterator = boost::stl_interfaces::reverse_iterator<view_iterator>; constexpr reverse_view() = default; #if BOOST_STL_INTERFACES_USE_CONCEPTS template<typename V> requires std::is_same_v<std::remove_reference_t<V>, View> #else template< typename V, typename E = std::enable_if_t< std::is_same<std::remove_reference_t<V>, View>::value>> #endif constexpr reverse_view(int, V && v) : v_{(V &&) v} { // To keep the code simpler, we just store the iterator to the end // of v, whether v is a common_range or has different iterator and // sentinel types. first_ = set_rev_rng_first< std::is_same<view_iterator, view_sentinel>::value>::call(v_); } constexpr iterator begin() const { return first_; } constexpr iterator end() const { return boost::stl_interfaces::make_reverse_iterator(v_.begin()); } // Return the underlying view that this view reverses. constexpr View base() const { return v_; } private: View v_ = View(); iterator first_; }; // is_reverse_view lets us detect construction of a reverse_view from // another reverse_view, and take appropriate action (see below). template<typename T> struct is_reverse_view : std::false_type {}; template<typename T> struct is_reverse_view<reverse_view<T>> : std::true_type {}; #if defined(__cpp_deduction_guides) template<typename R> reverse_view(int, R &&)->detail::reverse_view<std::remove_reference_t<R>>; #endif
As with all()
,
we define a closure
object
reverse
that we can invoke:
// We want to condition how we construct our view based on whether R is itself // a reverse_view. If R is a reverse_view, just return the view it's // reversing. // // In C++20 and later, you might want to constrain this lambda to require that // R is a std::ranges::view, since that's what reverse_view requires. inline constexpr boost::stl_interfaces::closure reverse = []<typename R>(R && r) { if constexpr (detail::is_reverse_view<std::decay_t<R>>::value) { return ((R &&) r).base(); } else { return detail::reverse_view(0, (R &&) r); } };
And again, we do this:
// Don't forget to designate our view as a borrowed range. #if BOOST_STL_INTERFACES_USE_CONCEPTS namespace std::ranges { template<typename View> inline constexpr bool enable_borrowed_range<detail::reverse_view<View>> = true; } #endif
Now that we have all()
and reverse()
,
we can write this:
for (auto x : all(range) | reverse) { // etc. }
Since everythinge is std
-compatible,
we can also write:
for (auto x : all(range) | reverse | std::views::reverse) { // etc. }
Unfortunately, reverse
and
std::views::reverse
know nothing about one another, so all(range)
| reverse
| std::views::reverse
does not reduce to all(range)
as both all(range)
| reverse
| reverse
and all(range) |
std::views::reverse
| std::views::reverse
do.
take()
take_view
is a little different
from the previous two views, in that it takes more than just a range to construct
it — it is constructed from a range and a count. Any time a view takes
more than just a range to construct, it is usually nice to use it with all
the parameters besides the view itself. For instance, you might want to use
take as v |
take(42)
, or as take(v, 42)
(more on this below). Here is our definition of take_view
:
// This is a really simple iterator that converts the given iterator Iter // to a forward_iterator that counts how many times it has ben // incremented. It counts down from an initial count to zero. template<typename Iter> struct take_iterator : boost::stl_interfaces::iterator_interface< take_iterator<Iter>, std::forward_iterator_tag, typename std::iterator_traits<Iter>::value_type, typename std::iterator_traits<Iter>::reference, typename std::iterator_traits<Iter>::pointer, typename std::iterator_traits<Iter>::difference_type> { constexpr take_iterator() = default; constexpr explicit take_iterator(Iter it, int n) : it_(std::move(it)), n_(n) {} constexpr Iter base() const { return it_; } constexpr int count() const { return n_; } constexpr take_iterator & operator++() { ++it_; --n_; return *this; } private: friend boost::stl_interfaces::access; constexpr Iter & base_reference() { return it_; } constexpr Iter const & base_reference() const { return it_; } template<typename Iter2> friend struct take_iterator; Iter it_; int n_; }; // This sentinel compares equal to any take_iterator whose count has // reached zero, or the end of the underlying range if that comes first. template<typename Sentinel> struct take_sentinel { take_sentinel() = default; explicit take_sentinel(Sentinel sent) : sent_(sent) {} template<typename Iter> friend constexpr bool operator==(take_iterator<Iter> it, take_sentinel s) { return !it.count() || it.base() == s.sent_; } template<typename Iter> friend constexpr bool operator!=(take_iterator<Iter> it, take_sentinel s) { return !(it == s); } private: Sentinel sent_; }; // The take_iterator and take_sentinel templates do all the hard work, // which leaves take_view quite simple. #if BOOST_STL_INTERFACES_USE_CONCEPTS template<std::ranges::view View> requires std::is_object_v<View> #else template< typename View, typename Enable = std::enable_if_t<std::is_object<View>::value>> #endif struct take_view : boost::stl_interfaces::view_interface<take_view<View>> { using iterator = take_iterator<iterator_t<View>>; using sentinel = take_sentinel<sentinel_t<View>>; // We don't need a phony initial int param for this constructor, since // it already takes two parameters; it won't get confused for a copy // or a move. The count here is just an int to keep things simple. #if BOOST_STL_INTERFACES_USE_CONCEPTS template<typename View2> requires std::is_same_v<std::remove_reference_t<View2>, View> #else template< typename View2, typename E = std::enable_if_t< std::is_same<std::remove_reference_t<View2>, View>::value>> #endif explicit take_view(View2 && r, int n) : first_(r.begin(), n), last_(r.end()) {} iterator begin() const { return first_; } sentinel end() const { return last_; } private: iterator first_; sentinel last_; }; #if defined(__cpp_deduction_guides) template<typename R> take_view(R &&, int)->detail::take_view<std::remove_reference_t<R>>; #endif
Now we need an invocable for users to call. Instead of using closure
, we use adaptor
:
// Use the adaptor template to support calling the given lambda with either // all the parameters or all the parameters after the first. inline constexpr boost::stl_interfaces::adaptor take = []<typename R>(R && r, int n) { return detail::take_view((R &&) r, n); };