Table of Contents

In this article, we’d shed some light on the implementation of ranges::reverse_view and std::views::reverse. We’ll compare them to understand the differences between views and their adaptor objects.

Let’s jump in.

ranges::reverse_view and std::views::reverse in Action  

Let’s look at an example to understand how these views work. Assume we have a range r of integers from 1 to 5. When we apply std::views::reverse to r, it creates a view representing the elements of r in the reverse order.

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> r = {1, 2, 3, 4, 5};
    auto reversed = r | std::views::reverse;
    for (auto i : reversed)
        std::cout << i << " ";
        
    // same as:
    for (auto i : r | std::views::reverse)
        std::cout << i << " ";
}

Similarly, we can use ranges::reverse_view to achieve the same result:

int main() {
    std::vector<int> r = {1, 2, 3, 4, 5};

    std::ranges::reverse_view rv(r);

    for (auto i : rv)
        std::cout << i << " ";
    }
}

In both cases we can expect the same, boring, result:

5 4 3 2 1

Run examples at @Compiler Explorer

At first glance, std::views::reverse and ranges::reverse_view seem to provide the same functionality, but there are differences in how they are implemented and the capabilities they provide.

Diving into the Implementation  

The ranges::reverse_view is a range adaptor representing a view of an underlying range but in reversed order. The ranges::reverse_view class provides several member functions, including a constructor to create the reverse_view, base to return the underlying view V, begin and end to return the beginning and end iterator of the reverse_view, respectively, and size to return the size of the view if it is bounded.

The std::views::reverse, on the other hand, is a range adaptor object.

The expression views::reverse(e) is equivalent to one of several different expressions depending on the type of e.

  • If the type of e is a specialization of reverse_view, e.base() is returned.
  • Otherwise, if the type of e is a ranges::subrange of std:: reverse_iterator, a subrange is constructed with the base and end iterators of e.
  • If neither of these conditions are met, ranges::reverse_view{e} is returned.

Essentially, views::reverse unwraps reversed views if possible.

MSVC Version  

Below we can find an implementation from the MSVC compiler/STL in the ranges header:

class _Reverse_fn : public _Pipe::_Base<_Reverse_fn> {
private:
    enum class _St { _None, _Base, _Subrange_unsized, _Subrange_sized, _Reverse };

    template <class>
    static constexpr auto _Reversed_subrange = -1;

    template <class _It, subrange_kind _Ki>
    static constexpr auto _Reversed_subrange<subrange<reverse_iterator<_It>, reverse_iterator<_It>, _Ki>> =
        static_cast<int>(_Ki);

    template <class _Rng>
    _NODISCARD static _CONSTEVAL _Choice_t<_St> _Choose() noexcept {
        using _Ty = remove_cvref_t<_Rng>;

        if constexpr (_Is_specialization_v<_Ty, reverse_view>) {
            if constexpr (_Can_extract_base<_Rng>) {
                return {_St::_Base, noexcept(_STD declval<_Rng>().base())};
            }
        } else if constexpr (_Reversed_subrange<_Ty> == 0) {
            using _It = decltype(_STD declval<_Rng&>().begin().base());
            return {_St::_Subrange_unsized,
                noexcept(subrange<_It, _It, subrange_kind::unsized>{
                    _STD declval<_Rng&>().end().base(), _STD declval<_Rng&>().begin().base()})};
        } else if constexpr (_Reversed_subrange<_Ty> == 1) {
            using _It = decltype(_STD declval<_Rng&>().begin().base());
            return {_St::_Subrange_sized,
                noexcept(subrange<_It, _It, subrange_kind::sized>{_STD declval<_Rng&>().end().base(),
                    _STD declval<_Rng&>().begin().base(), _STD declval<_Rng&>().size()})};
        } else if constexpr (_Can_reverse<_Rng>) {
            return {_St::_Reverse, noexcept(reverse_view{_STD declval<_Rng>()})};
        }

        return {_St::_None};
    }

    template <class _Rng>
    static constexpr _Choice_t<_St> _Choice = _Choose<_Rng>();

public:
    template <viewable_range _Rng>
        requires (_Choice<_Rng>._Strategy != _St::_None)
    _NODISCARD constexpr auto operator()(_Rng&& _Range) const noexcept(_Choice<_Rng>._No_throw) {
        constexpr _St _Strat = _Choice<_Rng>._Strategy;

        if constexpr (_Strat == _St::_Base) {
            return _STD forward<_Rng>(_Range).base();
        } else if constexpr (_Strat == _St::_Subrange_unsized) {
            return subrange{_Range.end().base(), _Range.begin().base()};
        } else if constexpr (_Strat == _St::_Subrange_sized) {
            return subrange{_Range.end().base(), _Range.begin().base(), _Range.size()};
        } else if constexpr (_Strat == _St::_Reverse) {
            return reverse_view{_STD forward<_Rng>(_Range)};
        } else {
            static_assert(_Always_false<_Rng>, "Should be unreachable");
        }
    }
};

_EXPORT_STD inline constexpr _Reverse_fn reverse;

Here’s a breakdown of the major components:

  1. Enum class (_St): This is an enumeration used to represent different strategies that can be used to reverse a range. It includes options like _Base (extracting the base range of a reverse_view), _Subrange_unsized and _Subrange_sized (creating a subrange with reverse iterators), and _Reverse (forming a reverse_view).
  2. Static constexpr functions (_Reversed_subrange, _Choose): These functions are used to compile-time determine which strategy should be used to reverse a given range. _Reversed_subrange checks if the range is already a subrange with reverse iterators, and _Choose selects the appropriate strategy based on the range type.
  3. _Choice_t<_St> _Choice: This is a type that represents the chosen strategy and whether the chosen strategy can be used without throwing an exception. It is computed at compile time for each range type.
  4. operator() function: This is the function call operator that implements the reversing of the range. It uses the strategy chosen by _Choose and applies it to the range.

This implementation ensures that reversing a range is as efficient as possible, by selecting the most appropriate strategy at compile time based on the properties of the range type.

For example, if the range is already a reverse_view, it can simply extract the base range, avoiding the need to create a new reverse_view. If the range is a subrange with reverse iterators, it can directly use these iterators to create a new subrange. If none of these specialized strategies apply, it falls back to creating a reverse_view.

Pay attention to the last line that defines std::views::reverse :

_EXPORT_STD inline constexpr _Reverse_fn reverse;

As you can see, it’s a callable object, rather than a simple function.

Differences and Use Cases  

Let’s have a look at some differences in detail:

Reverse of a reverse:  

std::vector<int> nums = {1, 2, 3, 4, 5};
{
    auto rev_view = nums | std::views::reverse;
    auto rev_rev_view = rev_view | std::views::reverse;

    for (int n : rev_rev_view)
        std::cout << n << ' ';

    std::cout << '\n' << typeid(rev_view).name() << '\n';
    std::cout << typeid(rev_rev_view).name() << '\n';
}

{
    std::ranges::reverse_view rev_view(nums);
    std::ranges::reverse_view rev_rev_view(rev_view); // won't "cancel out"!

    for (int n : rev_rev_view)
        std::cout << n << ' ';

    std::cout << '\n' << typeid(rev_view).name() << '\n';
    std::cout << typeid(rev_rev_view).name() << '\n';
}

The output on GCC:

1 2 3 4 5 
NSt6ranges12reverse_viewINS_8ref_viewISt6vectorIiSaIiEEEEEE
NSt6ranges8ref_viewISt6vectorIiSaIiEEEE
5 4 3 2 1 
NSt6ranges12reverse_viewINS_8ref_viewISt6vectorIiSaIiEEEEEE
NSt6ranges12reverse_viewINS_8ref_viewISt6vectorIiSaIiEEEEEE

As you can see, the view version doesn’t cancel out the initial reverse view!

Iterators  

auto rev_it_range = std::ranges::subrange(nums.rbegin(), nums.rend());
auto rev_subrange = rev_it_range | std::views::reverse;

    for (int n : rev_subrange)
        std::cout << n << ' ';

    std::cout << '\n' << typeid(rev_it_range).name() << '\n';
    std::cout << typeid(rev_subrange).name() << '\n';
}

{
    auto rev_it_range = std::ranges::subrange(nums.rbegin(), nums.rend());
    std::ranges::reverse_view rev_rev_it_range(rev_it_range);

    for (int n : rev_rev_it_range)
        std::cout << n << ' ';
    
    std::cout << '\n' << typeid(rev_it_range).name() << '\n';
    std::cout << typeid(rev_rev_it_range).name() << '\n';
}

Possible output from GCC:

1 2 3 4 5 
NSt6ranges8subrangeISt16reverse_iteratorIN9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEEES9_LNS_13subrange_kindE1EEE
NSt6ranges8subrangeIN9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEES7_LNS_13subrange_kindE1EEE
1 2 3 4 5 
NSt6ranges8subrangeISt16reverse_iteratorIN9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEEES9_LNS_13subrange_kindE1EEE
NSt6ranges12reverse_viewINS_8subrangeISt16reverse_iteratorIN9__gnu_cxx17__normal_iteratorIPiSt6vectorIiSaIiEEEEESA_LNS_13subrange_kindE1EEEEE

The output will be the same this time, but notice that rev_subrange has a much simpler type than rev_rev_it_range.

You can play with the whole example @Compiler Explorer

This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.

Other view adaptors  

I took reverse views as they seemed to be the most interesting and contained a bit of extra logic to select the best option for the view. But we can quickly compare other views and view adapter objects.

For example, the filter:

namespace views {
    struct _Filter_fn {
        // clang-format off
        template <viewable_range _Rng, class _Pr>
        _NODISCARD constexpr auto operator()(_Rng&& _Range, _Pr&& _Pred) const noexcept(noexcept(
            filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred)))) requires requires {
            filter_view(static_cast<_Rng&&>(_Range), _STD forward<_Pr>(_Pred));
        } {
            // clang-format on
            return filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred));
        }

        template <class _Pr>
            requires constructible_from<decay_t<_Pr>, _Pr>
        _NODISCARD constexpr auto operator()(_Pr&& _Pred) const
            noexcept(is_nothrow_constructible_v<decay_t<_Pr>, _Pr>) {
            return _Range_closure<_Filter_fn, decay_t<_Pr>>{_STD forward<_Pr>(_Pred)};
        }
    };

    _EXPORT_STD inline constexpr _Filter_fn filter;
} // namespace views

As you can see, filter always creates filter_view through the adaptor object.

And some other views:

  • take_view/take_fn also contains some logic to select the best option; see at MSVC/STL/ranges - for example it can return std::span or even std::string_view in certain situations.
  • drop_view/drop_fn similarly as take_fn it can also select optimal option, see at MSVC/STL/ranges
  • counted and counted_fn
  • all - and all_fn

Conclusion  

Both ranges::reverse_view and std::views::reverse provide powerful features to reverse a range, but they differ in handling types of input ranges. Generally, it’s best to use the views::reverse adaptor object, as it can select the best and optimal range.

The implementation of the Standard Library also contains other views and their adaptor objects that have extra logic. That way, those adaptors don’t always return a view but sometimes try with even string_view, spans, and other ranges.

As we concluded in C++20 Ranges: The Key Advantage - Algorithm Composition - C++ Stories

Always prefer views::meow over ranges::meow_view, unless you have a very explicit reason that you specifically need to use the latter - which almost certainly means that you’re in the context of implementing a view, rather than using one.