8

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

 1 year ago
source link: https://mariusbancila.ro/blog/2023/01/16/how-to-make-chunks-of-a-range-in-cpp23/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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].

ranges_adjacent_view.png

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';
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]
[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';
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>;
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';
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.

ranges_slide_view-1.png

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);
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]
[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';
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.

ranges_chunk_view-1.png

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);
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]
[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.

ranges_chunk_by_view.png
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);
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]
[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);
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]
[7,8,9]
[f,g,h,i]
[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:

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK