C++ code samples before and after Ranges

The Ranges library proposal has been accepted for C++20 at the San Diego meeting of the standard committee in November last year. The library provides components for handling ranges of values aimed at simplifying our code. Unfortunately, the Ranges library is not very well documented, which makes it harder to grasp for those that want to learn it. This post is intended as an introduction based on examples of code written with and without Ranges.

Eric Niebler’s implementation of the Ranges library is available here. It works will Clang 3.6.2 or later, gcc 5.2 or later, and VC++ 15.9 or later. The code samples below were written and tested with the latter. On a side note, these samples represent typical implementations and not necessarily the only solutions one could think of.

Although the standard namespace for the Ranges library is std::ranges, in this current implementation of the library it is ranges::v3.

The following namespace aliases are used in the samples below:

namespace rs = ranges::v3;
namespace rv = ranges::v3::view;
namespace ra = ranges::v3::action;

Also, for simplicity, we will refer to the following object, function, and lambda functions:

std::string to_roman(int value)
{
   std::vector<std::pair<int, char const*>> roman
   {
      { 1000, "M" },{ 900, "CM" },
      { 500, "D" },{ 400, "CD" },
      { 100, "C" },{ 90, "XC" },
      { 50, "L" },{ 40, "XL" },
      { 10, "X" },{ 9, "IX" },
      { 5, "V" },{ 4, "IV" },
      { 1, "I" }
   };

   std::string result;
   for (auto const & [d, r]: roman)
   {
      while (value >= d)
      {
         result += r;
         value -= d;
      }
   }

   return result;
}

std::vector<int> v{1,1,2,3,5,8,13,21,34};

auto print_elem = [](auto const e) {std::cout << e << '\n'; };

auto is_even = [](auto const i) {return i % 2 == 0; };

UPDATE

I would like to thank Eric Niebler and all the others that commented below with suggestions for these code samples. I have updated a few based on their feedback.

👉 Print the all the elements of a range

Before ranges:

std::for_each(
   std::cbegin(v), std::cend(v), 
   print_elem);

// or 
   
for(auto const i : v) 
{
   print_elem(i); 
};

After ranges:

rs::for_each(
   std::cbegin(v), std::cend(v), 
   print_elem);

// or

rs::for_each(std::as_const(v), print_elem);

👉 Print all the elements of a range in reverse order

Before ranges:

std::for_each(
   std::crbegin(v), std::crend(v),
   print_elem);

After ranges:

rs::for_each(
   std::crbegin(v), std::crend(v),
   print_elem);

// or

for (auto const i : v | rv::reverse)
{
   print_elem(i);
};

👉 Print only the even elements of the range but in reverse order

Before ranges:

std::for_each(
   std::crbegin(v), std::crend(v),
   [print_elem](auto const i) {
      if(i % 2 == 0)
         print_elem(i); 
   });

After ranges:

for (auto const i : v 
                  | rv::reverse 
                  | rv::filter(is_even))
{
   print_elem(i);
};

👉 Skip the first two elements of the range and print only the even numbers of the next three in the range

Before ranges:

auto it = std::cbegin(v);
std::advance(it, 2);
auto ix = 0;
while (it != std::cend(v) && ix++ < 3)
{
   if (is_even(*it))
      print_elem(*it);
   it++;
}

After ranges:

for (auto const i : v 
                  | rv::drop(2)
                  | rv::take(3)
                  | rv::filter(is_even))
{
   print_elem(i);
};

👉 Print all numbers from 101 to 200

Before ranges:

for (int n = 101; n <= 200; ++n)
{
   print_elem(n);
}

After ranges:

for (auto n : rs::iota_view(101, 201))
{
   print_elem(n);
}

👉 Print all the Roman numerals from 101 to 200

To convert a number to its corresponding Roman numeral the function to_roman() shown ealier is used.

Before ranges:

for (int i = 101; i <= 200; ++i)
{
   print_elem(to_roman(i));
}

After ranges:

for (auto n : rs::iota_view(101, 201) 
            | rv::transform(to_roman))
{
   print_elem(n);
}

// or

rs::for_each(rv::iota(101, 201), 
             print_element, to_roman);

👉 Print the Roman numeral of the last three numbers divisible to 7 in the range [101, 200], in reverse order

Before ranges:

for (int n = 200, count=0; n >= 101 && count < 3; --n)
{
   if (n % 7 == 0)
   {
      print_elem(to_roman(n));
      count++;
   }
}

After ranges:

for (auto n : rs::iota_view(101, 201) 
            | rv::reverse
            | rv::filter([](auto v) {
                return v % 7 == 0; })
            | rv::transform(to_roman)
            | rv::take(3))
{
   print_elem(n);
}

👉 Create a range of strings containing the Roman numeral of the last three numbers divisible to 7 in the range [101, 200], in reverse order

Before ranges:

std::vector<std::string> v;
for (int n = 200, count = 0; 
     n >= 101 && count < 3; --n)
{
   if (n % 7 == 0)
   {
      v.push_back(to_roman(n));
      count++;
   }
}

After ranges:

auto v = rs::iota_view(101, 201)
       | rv::reverse
       | rv::filter([](auto v) {return v % 7 == 0; })
       | rv::transform(to_roman)
       | rv::take(3)
       | rs::to_vector;

👉 Modify an unsorted range so that it retains only the unique values but in reverse order

Before ranges:

std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };

std::sort(std::begin(v), std::end(v));
v.erase(
   std::unique(std::begin(v), std::end(v)), 
   std::end(v));
std::reverse(std::begin(v), std::end(v));

After ranges:

std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };

v = std::move(v) | 
    ra::sort | 
    ra::unique | 
    ra::reverse;

👉 Remove the smallest two and the largest two values of a range and retain the other ones, ordered, in a second range

Before ranges:

std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
std::vector<int> v2 = v;
std::sort(std::begin(v2), std::end(v2));
      
auto first = std::begin(v2);
std::advance(first, 2);
auto last = first;
std::advance(last, std::size(v2) - 4);

v2.erase(last, std::end(v2));
v2.erase(std::begin(v2), first);

After ranges:

std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
auto v2 = v | 
          rs::copy | 
          ra::sort | 
          ra::slice(2, rs::end - 2);

👉 Concatenate all the strings in a given range into a single value

Before ranges:

std::vector<std::string> words {
   "Lorem", " ", "ipsum", " ", 
   "dolor", " ", "sit", " ", 
   "amet"};

std::string text;
for (auto const & word : words)
   text += word;

After ranges:

std::vector<std::string> words {
   "Lorem", " ", "ipsum", " ", 
   "dolor", " ", "sit", " ", 
   "amet"};

std::string text = words | 
                   rs::move | 
                   ra::join;

👉 Count the number of words (as delimited by space) in a text

Before ranges:

auto text = "Lorem ipsum dolor sit amet";

std::istringstream iss(text);
std::vector<std::string> words(
   std::istream_iterator<std::string>{iss},
   std::istream_iterator<std::string>());
auto count = words.size();


// or

size_t count = 0;
std::vector<std::string> words;
std::string token;
std::istringstream tokenStream(text);
while (std::getline(tokenStream, token, ' '))
{
   ++count;
}

After ranges:

auto text = "Lorem ipsum dolor sit amet";

auto count = rs::distance(
   rv::c_str(text) | rv::split(' '));

16 Replies to “C++ code samples before and after Ranges”

  1. Thanks Marius!
    Just a quick one: the string concatenation example can be done with accumulate too (in both non-ranges and ranges versions).

  2. I like your namespace aliases! Are they in common use or is everyone making up their own?

    On the very last example: surely the “pre-Ranges” version would use `++count` in the loop, instead of populating a whole heap-allocated vector of heap-allocated strings purely in order to take its `.size()`. What would be the equivalent of that in the “post-Ranges” version, or does what’s there now already avoid materializing the range?

  3. I made up the namespace aliases; I do not know if others are using the same I have not seen lots of Ranges code.

    Yes, you are right about the last sample. Should you just want to count the words there is no need to populate a vector with them. Point taken, thank you.

  4. Wouldn’t it be more idiomatic to have the following for “Print the all the elements of a range”
    rs::for_each(std::as_const(v), print_elem);

  5. … but nice work apart from that. Oh, and the first example could be simplified to `rs::for_each(std::as_const(v),print_elem)`. And look into projections. For instance, instead of:

    “`
    for (auto n : rs::iota_view(101, 201)
    | rv::transform(to_roman))
    {
    print_elem(n);
    }
    “`

    you can write:

    “`
    rs::for_each(rv::iota(101, 201), print_element, to_roman);
    “`

  6. That was in no way my intention. I explicitly mentioned that the standard namespace is std::ranges, but in your current implementation of the library it is ranges::v3.

  7. You might want to use a reference in the parameters of `print_elem` and `is_even`. By the way what is wrong with not using `as_const` when passing `v` to `for_each`?

  8. Awesome post!

    I was totally convinced at this example: “Print the Roman numeral of the last three numbers divisible to 7 in the range [101, 200], in reverse order.”
    Here, I believe that the library beats even English! Really, the code is more readable that the above statement!

    Also at some places we are doing this:

    | rv::transform(to_roman)
    | rv::take(3)

    How about the following code?


    | rv::take(3)
    | rv::transform(to_roman)

    Will this be more efficient? Or does the library handles it in a way that it doesn’t have to apply `to_roman()` to all the elements, but just the 3?

  9. Surely “print the Roman numeral of the last three numbers divisible to 7 in the range [101, 200], in reverse order” can be done with a simple programmer-time optimisation:

    cout << "CXCVI\nCLXXXIX\nCLXXXII\n";

    🙂

  10. All the nice stuff with ranges come with a serious caveat: you will be falling asleep waiting for your programs to finish compiling :-/

  11. Thanks for your great blog. Are include file and std::ranges available with either or both of the following compilers?
    Clang++ 8.0.0 or GCC’s g++ 9.1.0 compilers?

    If not any other C++ compiler(s)? And link(s) for downloading?

    Thanks

  12. In my previous comment, I meant to type Are include ranges file and std::ranges available …. ?
    Thanks

  13. No argument, ranges makes for a more compact and expressive code in some case. But it also feels like you are trying to deliberately exagerrate the “unwieldiness” of the classic approach.

    * Firstly, since the code is non-generic, i.e. it works with `std::vector` specifically, there’s no need to employ `std::begin` and `std::end` in it. You could’ve simply used `v.begin()` and `v.end()`, which is cleaner and more compact.

    * Secondly, even if you prefer `std::begin` and `std::end` you could’ve relied upon ADL and shortened the calls to plain `begin(v)` and `end(v)`. No need to provide a qualified name.

    Meanwhile, the range version is deliberately “beautified” by relying upon a pre-defined short namespace alias `ra`.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.