5

Apologies for the title, if I knew how to better phrase it then google probably already helped me...

I would like to have an object Y, that represents a view of container X, so that when I iterate over Y, it's either forward or backward iteration of X. I would like to do it without copying the data, hence the new ranges library comes into mind.

std::vector x{};
auto z = some_condition ? x : (x | std::views::reverse);

Apparently the types of x and (x|...) are different. How can I make them consistent?

Edit: just found the following question asked 10 years ago, I guess what I am trying to find out is, does ranges make things easier now? Since that solution still requires the for-loop logic to be put into a separate function or lambda.

Enlico
  • 23,259
  • 6
  • 48
  • 102
QnA
  • 1,035
  • 10
  • 25
  • 4
    You just don't. They're different types. – Barry Aug 15 '21 at 20:36
  • @Barry ok... maybe it is too much to ask. I was hoping there is some base(interface like) class that all views are derived from, so that I can cast them to that interface which just have begin() and end(). – QnA Aug 15 '21 at 20:58
  • 1
    What're you intending to do with `z` assuming that you could construct such a thing? – Barry Aug 15 '21 at 22:04
  • Then I can do a for loop on z. The solutions in [How best to control iteration direction?](https://stackoverflow.com/questions/9022692/how-best-to-control-iteration-direction) involves a separate function or lambda, for the logics in the for-loop. I just wonder if `ranges` can help to avoid that. – QnA Aug 15 '21 at 22:08
  • In some cases, you might simply convert the result to a container (for ex. vector)... Or you could always create a class `view_selector` (left as an excercise for the reader). – Phil1970 Aug 16 '21 at 01:28
  • In c++23 you will likely be able to use `| ranges::to()` (with deduced element type). It's in relatively far future for now though. – Kaznov Aug 16 '21 at 16:08
  • I think any form of to_vector conversions would make new copies? would like to avoid that if possible – QnA Aug 17 '21 at 18:35

3 Answers3

1

Apparently the types of x and (x|...) are different. How can I make them consistent?

You could make them consistent by using a type-erasing view for the ranges.

However, you must decide whether the potential runtime cost of such view is worth the goal that you're trying to achieve.

does ranges make things easier now?

It doesn't have an effect on this as far as I can tell. The same issue exists with iterators as well as ranges. Both can be worked around using type-erasure at the cost of potential runtime overhead.

Standard library doesn't provide an implementation of such type erasing range, nor a type-erasing iterator so you'll have to write your own (or as nearly always, use one written by someone else).


You can alternatively solve the problem with ranges the analogous way as in the linked iterator question, by avoiding their use in a single expression:

if (some_condition) {
    auto z = x | std::views::all;
} else {
    auto z = x | std::views::reverse;
}
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • thanks, can you elaborate a bit on type-erasing view? I actually tried the std::views::all before posting here, and it also has a different type than reverse. In your code example, this `z` will not be available after the if...else clause, meaning I still need to wrap my for loop logic inside a lambda/function, and call that lambda/function in both if/else branches? if so, then it sounds like the same solution in "How best to control iteration direction?". – QnA Aug 16 '21 at 01:20
  • @QnA Let me repeat: "You can alternatively solve the problem with ranges the analogous way **as in the linked iterator question**" – eerorika Aug 16 '21 at 08:46
  • @QnA `can you elaborate a bit on type-erasing view?` Use `std::any` to wrap any iterator. – eerorika Aug 16 '21 at 08:46
1

I'd package them up in a variant.

First write:

template<class...Ts, class V=std::variant<std::decay_t<Ts>...>>
V pick(std::size_t i, Ts&&...ts );

that returns a variant with the ith argument held.

Then:

auto z = pick(some_condition?0:1, std::views::all(x), x | std::views::reverse);

Now your code runs via std::visit.

std::visit( [&](auto&& elems){
  for( auto&& elem: elems ) {
    // loop
  }
}, z );

pick implementation:

namespace impl {
  template<class...Ts, std::size_t...Is, class V=std::variant<std::decay_t<Ts>...>>
  V pick(std::index_sequence<Is...>, std::size_t i, Ts&&...ts )
  {
    using pF = V(*)(std::tuple<Ts&&...>);
    const pF pickers[] = {
      +[](std::tuple<Ts&&...> t)->V{
        return V( std::in_place_index<Is>, std::get<Is>(std::move(t)) );
      }...
    };
    return pickers[i]( std::forward_as_tuple(std::forward<Ts>(ts)...) );
  }
}
template<class...Ts, class V=std::variant<std::decay_t<Ts>...>>
V pick(std::size_t i, Ts&&...ts ) {
  return impl::pick( std::make_index_sequence<sizeof...(Ts)>{}, i, std::forward<Ts>(ts)... );
}

and a lazy-evaluation variant:

namespace impl {
  template<class...Fs, std::size_t...Is, class V=std::variant<std::invoke_result_t<Fs>...>>
  V lazy_pick(std::index_sequence<Is...>, std::size_t i, Fs&&...fs )
  {
    using pF = V(*)(std::tuple<Fs&&...>);
    const pF pickers[] = {
      +[](std::tuple<Fs&&...> t)->V{
        return V( std::in_place_index<Is>, std::get<Is>(std::move(t))() );
      }...
    };
    return pickers[i]( std::forward_as_tuple(std::forward<Fs>(fs)...) );
  }
}
template<class...Fs, class V=std::variant<std::invoke_result_t<Fs>...>>
V lazy_pick(std::size_t i, Fs&&...fs ) {
  return impl::lazy_pick( std::make_index_sequence<sizeof...(Fs)>{}, i, std::forward<Fs>(fs)... );
}

Live example.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This is clever, but is limited in that it really only works for ranges or contiguous containers like `vector`. This runs the risk of copying the container either from missing the `std::span` call or from not being contiguous and having a `span`-analogue. As far as I can tell, this would simply copy any non-contiguous container like `deque`, `map`, `unordered_map`, `set`, etc – Human-Compiler Aug 16 '21 at 01:06
  • @human I'm sure there is a trivial view wrapper. I just didn't look for one. – Yakk - Adam Nevraumont Aug 16 '21 at 02:05
  • 1
    You're looking for `views::all`. – Barry Aug 16 '21 at 14:17
  • @Yakk-AdamNevraumont thanks! didn't expect a simple question could trigger such an involved solution, but definitely learned a lot! – QnA Aug 17 '21 at 18:37
  • This is [another implementation](https://stackoverflow.com/a/63248534/11638718) of `pick` by Yakk, which uses `std::integral_constant`. – 康桓瑋 Aug 18 '21 at 04:15
1

Apparently the types of x and (x|...) are different. How can I make them consistent?

Don't. Let them remain different types, just pass them to things that don't care so much about one specific type.

You can move the use into a generic lambda, and then conditionally call it with either x or x | std::views::reversed

I.e. instead of

std::vector x{};
auto z = some_condition ? type_erase(x) : (x | std::views::reverse);
for (auto y : z) {
    /* stuff */
}
// assign some value?

you have

auto do_stuff = [](auto && z) { 
    for (auto y : z) {
        /* stuff */
    }
    // return some value?
};
std::vector x{};
some_condition ? do_stuff(x) : do_stuff(x | std::views::reversed);
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • thanks, this looks nice, although it still uses lambda, it's definitely an improvement over the solutions in the referenced question, with the lambda wrapping the for loop, rather than for loops (std::for_each) wrapping lambda. – QnA Aug 17 '21 at 17:15