﻿# C\+\+20 Ranges — Complete Guide

C\+\+20 Ranges, also known as STL v2, effectively replaces existing STL algorithms and facilities\. In this article, I will guide you through the changes that Ranges introduce, talk about Views, which are a new composable approach to algorithms and show examples of FizzBuzz using three different methods, all utilizing some aspects of Ranges\.


> We published and translated this article with the copyright holder's permission\\\. The author is Šimon Tóth\\\. The article was \[originally\]\(https://itnext\.io/c\-20\-ranges\-complete\-guide\-4d26e3511db0\) published on ITNEXT\\\.



![0895_C++_20_Ranges_CompleteGuide/image1.png](https://import.viva64.com/docx/blog/0895_C++_20_Ranges_CompleteGuide/image1.png)

Note, however, that Ranges are one of the features that landed in C\+\+20 in a half\-baked state\. C\+\+23 should bring us much closer to comprehensive support\. Some of the examples will therefore use the [range v3 library](https://github.com/ericniebler/range-v3)\.

## Ranges vs old STL

As already mentioned, ranges are a drop\-in replacement for STL\. However, they introduce both internal and user\-facing changes that overall improve their usefulness\.

### Concepts

Ranges rely on concepts to specify what types of parameters can participate in each overload\. Therefore, making a mistake when using ranges will lead to shorter and more to the point error messages\.

A typical example is trying to sort a std::list\. Unfortunately, this is an easy mistake to make if you are new to C\+\+\.

```cpp
#include <iostream>
#include <ranges>
#include <list>
#include <algorithm>
int main() {
    std::list<int> dt = {1, 4, 2, 3};
    std::ranges::sort(dt.begin(), dt.end());
    std::ranges::copy(dt.begin(), dt.end(), 
        std::ostream_iterator<int>(std::cout, ","));
}
```

Instead of receiving a confusing error about the minus operator, we now get the exact problem as the first error:

```cpp
include/c++/12.0.0/bits/ranges_algo.h:1810:14: note: because
'std::_List_iterator<int>' does not satisfy 'random_access_iterator'
```

We can inspect the concepts defined by the Ranges library, as these are part of the standard\. For example, the [concept of a range](https://en.cppreference.com/w/cpp/ranges/range) is very straightforward, and it simply mandates that the expressions _std::ranges::begin\(rng\)_ and _std::ranges::end\(rng\)_ are valid\. If you want to read up on concepts, check out [my concepts guide](https://itnext.io/c-20-concepts-complete-guide-42c9e009c6bf)\.

The fundamental change here is that _end\(\)_ no longer needs to return the same type as _begin\(\)_\. The returned sentinel only needs to be comparable to the iterator type returned by _begin\(\)_\.

Apart from simplifying certain use cases, it also allows for infinite ranges and potential performance improvement\.

```cpp
std::vector<int> dt = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::ranges::shuffle(dt, std::mt19937(std::random_device()()));
auto pos = std::ranges::find(dt.begin(), 
                             std::unreachable_sentinel,
                             7);
std::ranges::copy(dt.begin(), ++pos, 
                  std::ostream_iterator<int>(std::cout, ","));
```

The _std::unreachable\_sentinel_ always returns _false_ when compared to an iterator\. The compiler will therefore optimize out the boundary check _it \!\= end_ since this expression is then always _true_\.

We can only use this trick when we have a contextual guarantee that the algorithm will terminate without going out of bounds, but it brings algorithms on par with hand\-written code\.

And finally, with the introduction of the range concept, we can also save up on writing and use the range accepting variants of algorithms\.

```cpp
std::vector<int> dt = {1, 4, 2, 3};
std::ranges::sort(dt);
```

### Projections

A massive new feature that, on the surface, seems trivial is the support for projections\. A projection is a unary invocable that is applied to every element\.

This often completely removes the need to write complex lambdas, and when it doesn't, it simplifies them significantly\. An invocable is an extension of callable and also accepts member pointers\.

```cpp
struct Account {
    std::string owner;
    double value();
    double base();
};
std::vector<Account> acc = get_accounts();
// member
std::ranges::sort(acc,{},&Account::owner);
// member function
std::ranges::sort(acc,{},&Account::value);
// lambda
std::ranges::sort(acc,{},[](const auto& a) { 
    return a.value()+a.base(); 
});
```

Without projections, we would have to include this logic as part of a custom comparator\.

```cpp
std::vector<int> dt = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::vector<int> result;
std::ranges::transform(dt, 
                       dt | std::views::reverse,
                       std::back_inserter(result),
                       std::minus<void>(),
                       [](int v) { return v*v; },
                       [](int v) { return v*v; });
std::ranges::copy(result, 
                  std::ostream_iterator<int>(std::cout, ","));
```

This is a slight foreshadowing for views, but I wanted to include another example that utilized two ranges as input\. In such a case, we get two separate projections\. Note that these projections can also return different return types, as long as they match up with the operation \(here_ std::minus_\)\.

### The small stuff

One last "small" feature I wanted to mention here is the prevention of dangling iterators\. Mainly because even if you don't care for it, you might find use\-cases for this particular pattern in your codebase\.

```cpp
auto good = "1234567890";
auto sep1 = std::ranges::find(std::string_view(good), '0');
std::cout << *sep1 << "\n";
auto bad = 1234567890;
auto sep2 = std::ranges::find(std::to_string(bad), '0');
std::cout << *sep2 << "\n";
```

You might recognize the problem here\. If we weren't using range variants of the algorithms, the "bad" variant would crash at runtime\. However, with ranges, this code will not compile\. When a range\-based algorithm is invoked with a temporary range that owns its elements, the algorithm will return a special iterator _std::ranges::dangling_\.

Note that the first variant with a _std::string\_view_ will still work just fine\. String view is a type of range that doesn't own its elements, and its iterators are freestanding \(they don't depend on the instance of _string\_view_\), so it is perfectly valid to pass such temporary into a range\-based algorithm\.

To opt\-in your ranges to work as temporaries, you need to specialize the _enable\_borrowed\_range_ constant:

```cpp
template<typename T>
inline constexpr bool 
    std::ranges::enable_borrowed_range<MyView<T>> = true;
```

## Composable views

One of the core problems with old STL algorithms is that they are not easily composable\. As a result, the code using algorithms is often quite verbose and, when working with immutable data, requires additional copies\.

Views are trying to address this issue, making code that relies on standard algorithms less verbose and more explicit\.

### Views

Views are simply ranges that are cheap to copy and move \(in constant time\)\. Because of this, a view cannot own the elements it is viewing\. One exception is _std::views::single_ which owns the single element it is viewing\.

Views compose at compile time with the expectation that the compiler will inline the code\.

For example, the following code will print out the last three elements of the range\. We first reverse the range, then take the first three elements, and finally reverse the range again \(note that there is _std::views::drop_ that does this directly\)\.

```cpp
namespace rv = std::ranges::views;
std::vector<int> dt = {1, 2, 3, 4, 5, 6, 7};
for (int v : rv::reverse(rv::take(rv::reverse(dt),3))) {
    std::cout << v << ", ";
}
std::cout << "\n";
```

### View closure objects

Because of the often deep nesting, the functional syntax of composing views can be cumbersome to write and read\.

Fortunately, ranges bring us another approach for compositing views\. Views in the _std::views_ namespace are actually view closure objects\. These are inline constexpr constants with each _std::ranges::xxx\_view_ mapping to an std::views::xxx object\. These objects overload the _operator\(\)_ for functional syntax as seen above and _operator\|_ for pipe\-style compositing\.

```cpp
namespace rv = std::ranges::views;
std::vector<int> dt = {1, 2, 3, 4, 5, 6, 7};
for (int v : dt | rv::reverse | rv::take(3) | rv::reverse) {
    std::cout << v << ", ";
}
std::cout << "\n";
```

Note that while views do not own their elements, they do not change the mutability of underlying data\. Here, we iterate over odd elements of the array and multiply them by two\.

```cpp
namespace rv = std::ranges::views;
std::vector<int> dt = {1, 2, 3, 4, 5, 6, 7};
auto odd = [](std::integral auto v) { return v % 2 == 1; };
for (auto& v : dt | rv::filter(odd)) {
    v *= 2;
}
```

## FizzBuzz three ways

Let's have a look at some concrete examples of Ranges\. We will write three versions of FizzBuzz:

* a range\-ified coroutine generator
* a generative approach using algorithms
* a composition approach using views

As mentioned at the beginning of the article, the current support in C\+\+20 is a bit lacking\. Therefore, I will rely on the [range v3 library](https://github.com/ericniebler/range-v3)\.

### Coroutine generator

Writing a coroutine FizzBuzz generator is almost identical to the typical implementation:

```cpp
ranges::experimental::generator<std::string> fizzbuzz() {
    for (int i = 1; ; i++) {
        std::string result;
        if (i % 3 == 0) result += "Fizz";
        if (i % 5 == 0) result += "Buzz";
        if (result.empty()) co_yield std::to_string(i);
        else co_yield result;
    }
}
```

However, if we use the _generator<\>_ from range v3 library, we can also use the invoked coroutine as a range\.

```cpp
for (auto s : fizzbuzz() | ranges::views::take(20)) {
    std::cout << s << "\n";
}
```

The main magic here is in the implementation of the iterator type \(note [this code](https://en.cppreference.com/w/cpp/coroutine/coroutine_handle) is not from range v3 library\)\.

```cpp
// Resume coroutine to generate new value.
void operator++() { 
    coro_.resume(); 
}
// Grab current value from coroutine.
const T& operator*() const {
    return *coro_.promise().current_value;
}
// We are at the end if the coroutine is finished.
bool operator==(std::default_sentinel_t) const { 
    return !coro_ || coro_.done(); 
}
```

The _std::default\_sentinel\_t_ is a convenience type provided by the standard, intended to be used for distinguishing comparisons against the _end\(\)_\. With this, we simply need to return this iterator from the _generator<\>_ return type:

```cpp
Iter begin() {
    if (coro_) {
        coro_.resume();
    } 
    return Iter{cor_};
}
std::default_sentinel_t end() { 
    return {}; 
}
```

#### Generating using algorithms

We have quite a few options for the generative approach, the most obvious one being _generate\_n_ that will allow us to generate the output directly\.

```cpp
ranges::generate_n(
    std::ostream_iterator<std::string>(std::cout, "\n"), 
    20,
    [i = 0]() mutable {
        i++;
        std::string result;
        if (i % 3 == 0) result += "Fizz";
        if (i % 5 == 0) result += "Buzz";
        if (result.empty()) return std::to_string(i);
        return result;
});
```

### Compositing using views

Both of the previous approaches are very similar\. They both implement FizzBuzz procedurally\. However, we can also implement FizzBuzz in a completely different way\.

FizzBuzz includes two cycles\. Fizz with a period of three and Buzz with a period of five\.

```cpp
std::array<std::string, 3> fizz{"", "", "Fizz"};
std::array<std::string, 5> buzz{"", "", "", "", "Buzz"};
```

First, we need to turn these cycles into infinite ranges\.

```cpp
const auto inf_fizz = fizz | ranges::views::cycle;
const auto inf_buzz = buzz | ranges::views::cycle;
```

Then we can combine them using _zip\_with_:

```cpp
const auto inf_fizzbuzz = ranges::views::zip_with(
    std::plus<>(), 
    inf_fizz, 
    inf_buzz);
```

Now we have an infinite range where each 3rd element is "Fizz", each 5th element is "Buzz", each 15th element is "FizzBuzz", and the rest are empty strings\.

We are missing the plain numbers for the elements that are neither Fizz of Buzz\. So let's construct an infinite range of indices \(starting at one\):

```cpp
const auto indices = ranges::views::indices
    | ranges::views::drop(1);
```

And finally, we need to put these two ranges together and output the final result\.

```cpp
const auto final_range = ranges::views::zip_with(
    [](auto i, auto s) { 
        if (s.empty()) return std::to_string(i); 
        return s;
    },
    indices,
    inf_fizzbuzz
);
ranges::copy_n(ranges::begin(final_range), 20,
    std::ostream_iterator<std::string>(std::cout, "\n"));
```

### Links and technical notes

All code examples and scripts are available at: 

[https://github\.com/HappyCerberus/article\-cpp20\-ranges](https://github.com/HappyCerberus/article-cpp20-ranges)\.

The range v3 library used for FizzBuzz examples is available at: 

[https://github\.com/ericniebler/range\-v3](https://github.com/ericniebler/range-v3)\.

### Thank you for reading

Thank you for reading this article\. Did you enjoy it?

I also publish videos on [YouTube](https://www.youtube.com/c/%C5%A0imonT%C3%B3th/videos)\. Do you have questions? Hit me up on [Twitter](https://twitter.com/SimonToth83) or [LinkedIn](https://www.linkedin.com/in/simontoth)\.