Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is the documentation for an old version of Boost. Click here to view this page for the latest version.
PrevUpHomeNext

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


PrevUpHomeNext