...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Note | |
---|---|
All the member functions provided by |
view_interface
Template
C++20 contains a CRTP
template, std::ranges::view_interface
, which takes a range or view,
and adds all the operations that view types have, using only the range's/view's
begin()
and end()
.
This is a C++14-compatible version of that template.
As with iterator_interface
, view_interface
makes it possible to write very few operations — only begin()
and
end()
are actually used by view_interface
— and get
all the other operations that go with view types. The operations added depend
on what kinds of iterator and/or sentinel types your derived view type defines.
Here is the declaration of view_interface
:
template<typename Derived, element_layout Contiguity = element_layout::discontiguous> struct view_interface;
view_interface
only requires the derived type and an optional non-type template parameter
that indicates whether Derived
's
iterators are contiguous. The non-type parameter is necessary to support pre-C++20
code.
Note | |
---|---|
Proxy iterators are inherently discontiguous. |
In this example, we're implementing something very similar to std::ranges::drop_while_view
.
First, we need helper view types subrange
and all_view
, and a function
that takes a range and returns a view of the entire range, all()
:
// A subrange is simply an iterator-sentinel pair. This one is a bit simpler // than the one in std::ranges; its missing a bunch of constructors, prev(), // next(), and advance(). template<typename Iterator, typename Sentinel> struct subrange : boost::stl_interfaces::view_interface<subrange<Iterator, Sentinel>> { subrange() = default; constexpr subrange(Iterator it, Sentinel s) : first_(it), last_(s) {} constexpr auto begin() const { return first_; } constexpr auto end() const { return last_; } private: Iterator first_; Sentinel last_; }; // std::view::all() returns one of several types, depending on what you pass // it. Here, we're keeping it simple; all() always returns a subrange. template<typename Range> auto all(Range && range) { return subrange<decltype(range.begin()), decltype(range.end())>( range.begin(), range.end()); } // A template alias that denotes the type of all(r) for some Range r. template<typename Range> using all_view = decltype(all(std::declval<Range>()));
Note that subrange
is derived
from view_interface
, so it will have
all the view-like operations that are appropriate to its Iterator
and Sentinel
types.
With the helpers available, we can define drop_while_view
:
// Perhaps its clear now why we defined subrange, all(), etc. above. // drop_while_view contains a view data member. If we just took any old range // that was passed to drop_while_view's constructor, we'd copy the range // itself, which may be a std::vector. So, we want to make a view out of // whatever Range we're given so that this copy of an owning range does not // happen. template<typename Range, typename Pred> struct drop_while_view : boost::stl_interfaces::view_interface<drop_while_view<Range, Pred>> { using base_type = all_view<Range>; drop_while_view() = default; constexpr drop_while_view(Range & base, Pred pred) : base_(all(base)), pred_(std::move(pred)) {} constexpr base_type base() const { return base_; } constexpr Pred const & pred() const noexcept { return pred_; } // A more robust implementation should probably cache the value computed // by this function, so that subsequent calls can just return the cached // iterator. constexpr auto begin() { // We're forced to write this out as a raw loop, since no // std::-namespace algorithms accept a sentinel. auto first = base_.begin(); auto const last = base_.end(); for (; first != last; ++first) { if (!pred_(*first)) break; } return first; } constexpr auto end() { return base_.end(); } private: base_type base_; Pred pred_; }; // Since this is a C++14 and later library, we're not using CTAD; we therefore // need a make-function. template<typename Range, typename Pred> auto make_drop_while_view(Range & base, Pred pred) { return drop_while_view<Range, Pred>(base, std::move(pred)); }
Now, let's look at code using these types, including operations defined by
view_interface
that we did not have to write:
std::vector<int> const ints = {2, 4, 3, 4, 5, 6}; // all() returns a subrange, which is a view type containing ints.begin() // and ints.end(). auto all_ints = all(ints); // This works using just the used-defined members of subrange: begin() and // end(). assert( std::equal(all_ints.begin(), all_ints.end(), ints.begin(), ints.end())); // These are available because subrange is derived from view_interface. assert(all_ints[2] == 3); assert(all_ints.size() == 6u); auto even = [](int x) { return x % 2 == 0; }; auto ints_after_even_prefix = make_drop_while_view(ints, even); // Available via begin()/end()... assert(std::equal( ints_after_even_prefix.begin(), ints_after_even_prefix.end(), ints.begin() + 2, ints.end())); // ... and via view_interface. assert(!ints_after_even_prefix.empty()); assert(ints_after_even_prefix[2] == 5); assert(ints_after_even_prefix.back() == 6);
If you want more details on view_interface
, you can find
it wherever you usually find reference documentation on the standard library.
We won't cover it here for that reason. See [view.interface
on eel.is] or https://cppreference.com
for details.