Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++20 Streams aka Ranges

When I use the Stream Library (http://jscheiny.github.io/Streams/api.html#) I can do similar things like in Java-Streams:

#include "Streams/source/Stream.h"
#include <iostream>

using namespace std;
using namespace stream;
using namespace stream::op;

int main() {

    list<string> einkaufsliste = {
        "Bier", "Käse", "Wurst", "Salami", "Senf", "Sauerkraut"
    };

    int c = MakeStream::from(einkaufsliste)
          | filter([] (string s) { return !s.substr(0,1).compare("S"); })
          | peek([] (string s) { cout << s << endl; })
          | count()
          ;

    cout << c << endl;
}

It gives this output:

Salami
Senf
Sauerkraut
3

In C++20 I discovered ranges, which look promising to achieve the same thing. However, when I want to build something similar functional programming style it does not work:

#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>

using namespace std;

int main() {

    vector<string> einkaufsliste = {
        "Bier", "Käse", "Wurst", "Salami", "Senf", "Sauerkraut"
    };

    int c = einkaufsliste
          | ranges::views::filter([] (string s) { return !s.substr(0,1).compare("S"); })
          | ranges::for_each([] (string s) { cout << s << " "; })
          | ranges::count();
          ;
}

Seams that the ranges thing is not meant to work like this, although articles like this (https://www.modernescpp.com/index.php/c-20-the-ranges-library) suggest such a feature.

test.cpp:16:67: note:   candidate expects 3 arguments, 1 provided
   16 |             | ranges::for_each([] (string s) { cout << s << " "; })
      |                                                                   ^
test.cpp:17:29: error: no match for call to '(const std::ranges::__count_fn) ()'
   17 |             | ranges::count();
      |                             ^

Any ideas how I still could do similar things in C++20?

like image 528
Matthias Avatar asked Feb 20 '21 23:02

Matthias


2 Answers

There is an issue with each adapter here.


First, filter:

| ranges::views::filter([] (string s) { return !s.substr(0,1).compare("S"); })

This copies every string, then creates a new string out of each one, all just to check if the first character is an S. You should definitely take the string by const& here. And then, since the question is tagged C++20:

| ranges::views::filter([](string const& s) { return !s.starts_with('S'); })

Second, for_each:

| ranges::for_each([] (string s) { cout << s << " "; })

ranges::for_each is not a range adapter - it is an algorithm that invokes the callable on each element, but it doesn't return a new range, so it can't fit in a pipeline like this.

Ranges does not have a peek adapter like this (neither C++20 nor range-v3) But we could try to implement it in terms of a transform using identity:

auto peek = [](auto f){
    return ranges::views::transform([=]<typename T>(T&& e) -> T&& {
        f(e);
        return std::forward<T>(e);
    });
};

And now you can write (and again, the string should be taken as const& here):

| peek([](std::string const& s){ cout << s << " "; })

But this will actually only work if we access any of the elements of the range, and nothing in your code has to do that (we don't need to read any of the elements to find the distance, we just need to advance the iterator as many times as necessary). So you'll find that the above doesn't actually print anything.

So instead, for_each is the correct approach, we just have to do it separately:

auto f = einkaufsliste
       | ranges::views::filter([](string const& s) { return s.starts_with('S'); });
ranges::for_each(f, [](string const& s){ cout << s << " "; });

That will definitely print every element.


Lastly, count:

| ranges::count();

In Ranges, only the adapters that return a view are pipeable. count() just doesn't work this way. Also, count() takes a second argument which is which thing you're counting. count(r, value) counts the instances of value in r. There is no unary count(r).

The algorithm you're looking for is named distance (and likewise, is not pipeable into).

So you'd have to write the whole thing like this:

int c = ranges::distance(f);

This was part of the motivation for P2011, to be able to actually write count at the end in linear order rather than at the front (without having to make a lot more library changes).

like image 165
Barry Avatar answered Oct 09 '22 22:10

Barry


Main problem with your attempt is that only views are "pipable", and algorithms such as std::ranges::for_each are not.

There isn't a necessity to cram all of the functionality into a single statement. Here is a correct way to do the same with C++20 ranges:

// note the use of std::string_view instead
// of std::string to avoid copying
auto starts_s_pred = [] (std::string_view s) {
    return s.starts_with("S");
};
auto starts_s_filtered =
    einkaufsliste
    | ranges::views::filter(starts_s_pred);

// we could use std::string_view too, but it is
// unnecessary to restrict the lambda argument
// since this can easily work with anything
// insertable into a string stream
auto print_line = [] (const auto& s) {
    std::cout << s << '\n';
};

ranges::for_each(starts_s_filtered, print_line);
std::cout << std::ranges::distance(starts_s_filtered);


// alternative to print_line and for_each
for (const auto& s : starts_s_filtered)
    std::cout << s << '\n';
like image 2
eerorika Avatar answered Oct 09 '22 21:10

eerorika