How to make chunks of a range in C++23

Last year, I wrote a blog post about new C++23 range adaptors for joining and zipping. The C++23 standard includes a longer lists of range adapters (you can find a list here). Among them, there are several adaptors used for creating views consisting of chunks or “windows” of the elements of a given range. In this blog post, I will present these adaptors.

adjacent

std::ranges::adjacent_view is a range adaptor that takes a view and produces a new view whose elements are windows of N adjacent elements of the original view. To understand this better, let’s consider we have a view consisting of the following elements: [1, 2, 3, 4, 5, 6, 7, 8]. You need to process these elements in chunks of 3 consecutive elements, but in a sliding manner: [1, 2, 3], [2, 3, 4], etc. up to [6, 7, 8].

You can do this as following:

std::vector v = { 1, 2, 3, 4, 5, 6, 7, 8 };
for (auto const& t : v | std::views::adjacent<3>)
{
   std::cout << '[' << std::get<0>(t) << ',' 
                    << std::get<1>(t) << ',' 
                    << std::get<2>(t) 
             << ']' << '\n';
} 

This prints the following to the output console:

[1,2,3]
[2,3,4]
[3,4,5]
[4,5,6]
[5,6,7]
[6,7,8]

Since the elements of the resulting view are tuples of references to elements of the original view, we can use structure binding to decompose the tuple and access it elements easier:

for (auto const& [a,b,c] : v | std::views::adjacent<3>)
{
   std::cout << '[' << a << ',' << b << ',' << c << ']' << '\n';
}

If the number of elements of each chunk (window) is 2, then you can use the pairwise view, which is defined as follows:

namespace views {
    inline constexpr auto pairwise = adjacent<2>;
}

Here is an example:

for (auto const& [a,b] : v | std::views::pairwise)
{
   std::cout << '[' << a << ',' << b << ']' << '\n';
}

The adjacent_view adaptor requires that the size of the chunks is provided at compile-time, which means it cannot be used if you only know it at runtime. However, another adaptor allows you to do just that.

slide

std::ranges::slide_view is a range adaptor that works just like adjacent_view, except that the chunk size is provided at runtime. Also, the elements of the result view are not tuples but other views.

Here is an example:

std::vector v = { 1, 2, 3, 4, 5, 6, 7, 8 };
for (auto const& c : v | std::views::slide(3))
{
   print(c);
}

This snippet will produce the same output we saw earlier:

[1,2,3]
[2,3,4]
[3,4,5]
[4,5,6]
[5,6,7]
[6,7,8]

For clarity, here is the implementation of the print() function used in this snippet:

template <typename R>
auto print(R&& r)
{
   std::cout << '[';
   bool first = true;
   for (auto const e : r)
   {
      if(first) first = false;
      else std::cout << ',';
       
      std::cout << e;
   }
   std::cout << ']' << '\n';
}

chunk

If you need to split the original view in chunks of N elements (and not produce chunks by “sliding” along the original view) you can use the std::ranges::chunk_view adaptor. The size of the chunk is a runtime value, just as for std::ranges::slide_view.

Here is an example:

std::vector v = { 1, 2, 3, 4, 5, 6, 7, 8 };
for (auto const& c : v | std::views::chunk(3))
{
   print(c);
}

This will produce the following output:

[1,2,3]
[4,5,6]
[7,8]

In practice, you might need to make this splitting not using a fixed size, but by some specific criteria. In this case, another range adaptor is available.

chunk_by

The std::ranges::chunk_by_view range adaptor is similar to std::ranges::chunk_view except that instead of providing a size, you provide a binary predicate. The splitting occurs between each pair of adjacent elements for which the predicate returns false. Let’s take a couple of examples to understand it.

In the first example, we want to split a view into multiple chunks so that each chunk contains elements so that each adjacent pair does not differ by more than 1.

bool differ_by_one(int const a, int const b)
{
    return std::abs(a - b) <= 1;
}

std::vector v = {1,1,2,3,2,2,1,3,4,8,7,6,7};
for (auto const& c : v | std::views::chunk_by(differ_by_one))
{
   print(c);
}

The predicate used here is differ_by_one, which returns true if the absolute value of the difference of two adjacent elements is 0 or 1. The result of splitting this vector with the differ_by_one predicate is the following chunks of elements:

[1,1,2,3,2,2,1]
[3,4]
[8,7,6,7]

For a second example, let’s consider we have an input text that contains sequences of digits and letters. We want to produce a view of each chunk of characters of the same kind (either digits or letters):

bool same_kind(char const a, char const b)
{
    bool b1 = std::isdigit(a);
    bool b2 = std::isdigit(b);

    return (b1 && b2) || (!b1 && !b2);
}

std::string s {"1234abc56e789fghi"};
for (auto const& c : s | std::views::chunk_by(same_kind))
{
   print(c);
}

This time, the predicate is the same_kind function. This returns true if both arguments are digits or neither is a digit. The output of this snippet is the following:

[1,2,3,4]
[a,b,c]
[5,6]
[e]
[7,8,9]
[f,g,h,i]

See Also

If you want to learn more about these range adaptors see the following:

1 Reply to “How to make chunks of a range in C++23”

  1. In the “bool same_kind(char const a, char const b)” example:
    return (b1 && b2) || (!b1 && !b2);
    could be simplified to:
    return b1 == b2;

Leave a Reply

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