| Home | Libraries | People | FAQ | More

## Tutorial: View Adaptors

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 `boost::stl_interfaces::range_adaptor_closure` an alias for `std::views::__adaptor::_RangeAdaptorClosure`. This is necessary because all the `std` views will only combine with views derived from `std::views::__adaptor::_RangeAdaptorClosure`. Even naming a namespace that contains `__` is technically UB. I do not expect you to encounter any real problems if you use `boost::stl_interfaces::range_adaptor_closure` with GCC 12, but I thought you should know.

#### Example: `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

#### Example: `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.

#### Example: `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<
    #if !BOOST_STL_INTERFACES_USE_DEDUCED_THIS
                  take_iterator<Iter>,
    #endif
                  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); };