C++ Logo

std-proposals

Advanced search

introduce slicing_iterator for further std::views and algorithm usage

From: kang hewill <hewillk_at_[hidden]>
Date: Sat, 17 Apr 2021 02:43:33 +0800
Hi

P2017R1 <http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/p2017r1.html>(*Conditionally
borrowed ranges*) proposes that several range adapters can be
considered as *conditionally
borrowed*, in order to work better with constrained algorithms. Take
ranges::elements_view as an example, if we allow it to be conditionally
borrowed, we can <https://godbolt.org/z/T317YP6a1> safely call the base() of
the return iterator with no concern:

std::vector<std::pair<int, int>> v{{0, 1}, {2, 3}};
auto it = ranges::find(v | views::keys, 0).base();

But there are also some range adapters that are not conditionally borrowed,
such as ranges::transform_view. The reason behind it is that it needs to
hold the predicate. Once it is destroyed, the predicate does not exist, so
the following code is ill-formed:

std::vector<std::string> v{"abc", "de"};
auto it = ranges::find(v | views::transform(&std::string::size), 2).base();

Since the range we passed in is not borrowed, the return type of
ranges::find is just ranges::dangling. I totally agree with this safety
concern. But generally speaking, *if we apply the standard algorithms to
range adapters, we are more concerned about the underlying range*. If we
only return ranges::dangling because of ranges::transform_view, we actually
lose too much information.

In other words, *if the underlying range is borrowed*, we can actually
return an iterator that wraps the underlying range. Here I call it
slicing_iterator, which, as its name describes, slices the range adapters
into its base, and this iterator only provides the base() function, which
is used to return the iterator of the underlying range. Of course, this is
only a concept I currently think of, so don't be too demanding.

According to this concept, std::borrowed_iterator_t can be further changed
to:

template <ranges::range R, >
using borrowed_iterator_t = std::conditional_t<
  ranges::borrowed_range<R>,
  ranges::iterator_t<R>,
  // note this line
  std::conditional_t<
    ranges::view<R> && ranges::borrowed_range<ranges::view_base_t<R>>,
    std::slicing_iterator<ranges::view_base_t<R>>,
    ranges::dangling
>
>;

So the following code will be well-formed:

std::vector<std::string> v{"abc", "de"};
// sit type is std::slicing_iterator<ranges::ref_view<std::vector<std::string>>>
auto sit = ranges::find(v | views::transform(&std::string::size), 2);

// we can call base() to get the underlying iterator, which is
__gnu_cxx::__normal_iterator
auto it = sit.base();

// we can using one line to print "de"
std::cout << *ranges::find(v | views::transform(&std::string::size),
2).base() << "\n";

// we can using one line to print "abc"
std::cout << *ranges::find(v | views::filter([](auto) { return 1; }),
"abc").base() << "\n";

// note that two ranges::find's return type are the same,
// which is std::slicing_iterator<ranges::ref_view<std::vector<std::string>>>
static_assert(std::same_as<
  decltype(ranges::find(v | views::filter(...), 2)),
  decltype(ranges::find(v | views::transform(...), "abc"))
>);

What do you think?

Received on 2021-04-16 13:43:49