简体   繁体   中英

C++20: Why can't range adaptors and ranges be combined in one expression?

I am looking at the following snippet:

std::vector<int> elements{ 1,2,3 };
// this won't compile:
elements
    | std::views::filter([](auto i) { return i % 2 == 0; })
    | std::ranges::for_each([](auto e) {std::cout << e << std::endl; });
// but this compiles:
auto view  = 
elements
    | std::views::filter([](auto i) { return i % 2 == 0; });
std::ranges::for_each(view, [](auto e) {std::cout << e << std::endl; });
 

and I can't see the reason why the combination of the range adaptor (view) and the range algorithm via the pipe operator does not compile. I suppose it has something to do with the type std::ranges::dangling that for_each seems to be returning as my compiler (msvc 19.29) reports, but I am not sure ...

Why can't range adaptors and ranges be combined in one expression?

They can be. You just did combine them. You used elements as a range, and combined it with the range adaptor std::views::filter .

std::ranges::for_each however is neither a range, nor is it a range adaptor. It is a function (template) that accepts range as an argument. You can keep it in one expression if you really want to, although at the cost of readability in my opinion:

std::ranges::for_each(
    elements
      | std::views::filter([](auto i) { return i % 2 == 0; }),
    [](auto e) {std::cout << e << std::endl; }
);

There isn't really any inherent design reason why some sets of algorithms do have pipe support (views, actions, and to - although C++20 only has views so far) and others don't (everything else in <algorithm> ). Piping into for_each makes just as much sense as piping into filter , it's just that range-v3 (and, consequently, C++20 Ranges) only did so for some things and not others.

As of now, there is no opt-in mechanism to make something "pipeable" in the Ranges sense (see P2387 to change that). But once that paper, or some variant of it, gets approved, it would become easy to write an algorithm adapter that simply makes algorithms pipeable. So that you could do, for instance, this:

elements
  | std::views::filter([](auto i) { return i % 2 == 0; })
  | adaptor(std::ranges::for_each)([](auto e) {std::cout << e << std::endl; });

Demo (note that this depends on libstdc++ internals at the moment).

One issue with the library machinery approach to this problem is ambiguity - this relies on deciding that if f(x, y) is invokable that the intent is to make it a full call rather than a partial one. It's certainly possible for some algorithms that you may not be able to distinguish between a partial and full call in this sense (which itself may be one motivation for not having them be implicitly pipeable). This is certainly a known problem with some views already ( zip , concat , and join taking a delimiter are examples) and is itself one of the motivations for having a language solution to this problem (eg P2011 ).

Of course that only affects whether you could write | some_algo(args...) | some_algo(args...) , the opt-in mechanism that I'm describing here could just as easily have been spelled | partial(ranges::for_each, args...) | partial(ranges::for_each, args...) instead of | adaptor(ranges::for_each)(args...) | adaptor(ranges::for_each)(args...) , and then there'd be no ambiguity. It's just a weirder spelling in my opinion, which is why I showed it the way I did.

filter is a range adaptor: it takes a range of values and converts it into a different range of values.

for_each is an operation . It takes a range of values and does something with it. That "something" does not led to the production of a range of values. So it's not adapting anything; it is consuming the range.

And range adaptor composition is about building ranges, not consuming them. The product of a range composition is always a range.

What you're asking is no different from wondering why you can add one std::string to another, but you can't add a std::string to an std::ifstream to open a file. Opening a file doesn't produce a string; it produces a file . Same goes here: performing an arbitrary operation over a range doesn't produce a range.

This is why ranges::transform has a range adaptor views::transform version: because transform conceptually produces a new range of values. for_each doesn't produce anything; it merely consumes a range.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM