QnA
QnA

Reputation: 1103

c++20 ranges library, how to make conditional operator work?

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.

Upvotes: 6

Views: 1205

Answers (4)

battlmonstr
battlmonstr

Reputation: 6290

In C++23 in this case it is doable using zip_transform:

std::vector<int> v = {1, 2, 3};
bool is_reverse = false;

auto forward_or_reverse_item = [is_reverse](int forward_item, int reverse_item) {
        return is_reverse ? reverse_item : forward_item;
};

auto forward_or_reverse_view = std::views::zip_transform(
    forward_or_reverse_item, v, v | std::views::reverse);

godbolt example

This is not ideal, because it performs double iteration. In this case it is not a big problem, but if the initial view is not a plain vector, and performs significant computation to produce items or if it has side effects, then this approach becomes not viable.

Another approach is to use Range-v3 ranges::any_view type-erasure mechanism that makes both condition branches type-compatible, but supposedly might have some performance degradation.

Upvotes: 0

Caleth
Caleth

Reputation: 62704

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);

Upvotes: 4

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275405

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.

Upvotes: 1

eerorika
eerorika

Reputation: 238351

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;
}

Upvotes: 1

Related Questions