Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does view::join require copyable inner range? Why?

Suppose that we have

cppcoro::generator<int> gen_impl(int in) {
  const auto upper = in + 10;
  for (; in < upper; ++in)
    co_yield in;
}

cppcoro::generator<cppcoro::generator<int>> gen() {
  for (int n = 1; n < 100; n += 10)
    co_yield gen_impl(n);
}

So we can iterate inner range just fine

  for (auto&& row : gen() ) {
    for (auto n : row)
      std::cout << n << ' ';
    std::cout << '\n';
  }

NOTE: range-for on ref is required because cppcoro::generator doesn't allow copying (deleted copy ctor)

Print

1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50
51 52 53 54 55 56 57 58 59 60
61 62 63 64 65 66 67 68 69 70
71 72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89 90
91 92 93 94 95 96 97 98 99 100

But when we try to "flattern" with view::join

auto rng = gen();
for (auto n : rng | ranges::view::join) {
  std::cout << n << '\n';
};

It seems view::join require Copyable inner range?

In file included from <source>:3:

In file included from /opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/view.hpp:38:

In file included from /opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/view/for_each.hpp:23:

/opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/view/join.hpp:320:50: error: call to deleted constructor of 'cppcoro::generator<cppcoro::generator<int> >'

                    return join_view<all_t<Rng>>{all(static_cast<Rng&&>(rng))};

                                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

/opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/view/view.hpp:112:21: note: in instantiation of function template specialization 'ranges::v3::view::join_fn::operator()<cppcoro::generator<cppcoro::generator<int> > &, false, nullptr>' requested here

                    v.view_(static_cast<Rng&&>(rng))

                    ^

/opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/utility/functional.hpp:731:42: note: in instantiation of function template specialization 'ranges::v3::view::view<ranges::v3::view::join_fn>::pipe<cppcoro::generator<cppcoro::generator<int> > &, ranges::v3::view::view<ranges::v3::view::join_fn> &, false, nullptr>' requested here

            pipeable_access::impl<Pipe>::pipe(static_cast<Arg&&>(arg), pipe)

                                         ^

<source>:35:21: note: in instantiation of function template specialization 'ranges::v3::operator|<cppcoro::generator<cppcoro::generator<int> > &, ranges::v3::view::view<ranges::v3::view::join_fn>, false, nullptr>' requested here

  for (auto n : rng | ranges::view::join) {

                    ^

/opt/compiler-explorer/libs/cppcoro/include/cppcoro/generator.hpp:174:3: note: 'generator' has been explicitly marked deleted here

                generator(const generator& other) = delete;

                ^

/opt/compiler-explorer/libs/rangesv3/trunk/include/range/v3/view/join.hpp:76:36: note: passing argument to parameter 'rng' here

            explicit join_view(Rng rng)

                                   ^

What makes this not compiled?

Is there any bug in range-v3 or cppcoro?

Only incompatible design decisions?

godbolt (Full)

like image 262
sandthorn Avatar asked Nov 19 '18 10:11

sandthorn


1 Answers

In range-v3, a move-only view is OK. That got implemented late and there may still be bugs, but that's not what is happening here.

The first problem is that you are trying to adapt an lvalue of type cppcoro::generator here:

auto rng = gen();
for (auto n : rng | ranges::view::join) {

Since a generator is a view, the join view will want to copy it. It can't because it is not copyable.

You can fix this problem by moving the generator in:

auto rng = gen();
for (auto n : std::move(rng) | ranges::view::join) {

Then you run into the next problem, which is that the reference type of generator<generator<int>> is const generator<int>&, and you have the same problem again: the join wants to hold a copy of the inner generator while it iterates over it, but it cannot make a copy.

The workaround is a bit ugly: change the generator to return a non-const lvalue reference:

cppcoro::generator<cppcoro::generator<int>&> gen() {
  for (int n = 1; n < 100; n += 10) {
    auto tmp = gen_impl(n);
    co_yield tmp;
  }
}

and then std::move each inner range with a move view:

auto rng = gen();
for (auto n : std::move(rng) | ranges::view::move | ranges::view::join) {
  std::cout << n << '\n';
}

The result compiles. Whether it runs or not depends on how gracefully cppcoro handles the case where someone steals away the guts of the value that it safely tucked away in the coroutine's promise type.

https://godbolt.org/z/mszidX

A note about the future std::view::join:

The join view that will ship with C++20 is a little different. If the outer range's reference type is a real reference (as in this case), it will not try to make a copy of the view to which it refers. That means in C++20, you won't need the ugly view::move hack.

However, the C++20 View concept currently requires copyability so this solution still won't work. We have a TODO item to relax this before C++20 ships, but there's no telling how the Committee will like that idea.

like image 58
Eric Niebler Avatar answered Oct 04 '22 17:10

Eric Niebler