Last Update:
Understanding Ranges Views and View Adaptors Objects in C++20/C++23
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 ofreverse_view
,e.base()
is returned. - Otherwise, if the type of
e
is aranges::subrange
ofstd::
reverse_iterator, a subrange is constructed with the base and end iterators ofe
. - 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:
- 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). - 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. - _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.
- 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 returnstd::span
or evenstd::string_view
in certain situations.drop_view
/drop_fn
similarly astake_fn
it can also select optimal option, see at MSVC/STL/rangescounted
andcounted_fn
all
- andall_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
overranges::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.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: