#1179

ranges::base

Opentower120wants to merge intofrom

Abstract

This PR introduces ranges::base standalone function. It provides machinery for retrieving iterator of desired type from iterator base() chains (base().base().base()....).

Motivation

auto list = vec | view::transform(&Data::i) | view::take_exactly(3);
list.begin().base().base() != vec.begin();          // types mismatch

You have to "guess" count of base() calls to the desired layer of iterator adaptor.

auto list = vec | view::transform(&Data::i) | view::take_exactly(3);
base<iterator_t<Vec>>(list.begin()) == vec.begin();

Usage with algorithms:

auto list  = vec | view::transform(&Data::i) | view::take_exactly(3);
iterator i = base<iterator>(find(list, 10));

Interface

// iterator

ranges::base(iter);                     // iter.base()
ranges::base<3>(iter);                  // iter.base().base().base()
ranges::base<iterator_t<Range>>(iter);  // iter.base()...
ranges::base<Range>(iter);              // try ranges::base<iterator_t<Range>>(iter), then sentinel_t, then hard error
?? ranges::base(iter, range);              // ranges::base<decltype(range)>(iter)

// range  (just for symetry)

ranges::base(range);          // range.base()
ranges::base<3>(range);       // range.base().base().base()
ranges::base<List>(range);    // range.base()...

// range iterator_wise  (for algorithms which returns range)

ranges::base(ranges::iterator_wise, range);         // iterator_range(ranges::base(range.begin()), ranges::base(range.end()))
ranges::base<3>(ranges::iterator_wise, range);      // iterator_range(ranges::base<3>(range.begin()), ranges::base<3>(range.end()))
ranges::base<List>(ranges::iterator_wise, range);   // iterator_range(ranges::base<iterator_t<List>>(range.begin()), ranges::base<sentinel_t<List>>(range.end()))

Principle of operation

Iterator/range base() called until it's type does not match required. On type comparison references removed, constness remains.

WIP

Activity

8
T
af3172e
T
8e00186
T
277b116
ericniebler7y ago

I'm not a fan of this functionality. I don't see enough of a need, and I never want to call .base() an arbitrary number of times.

tower1207y ago

I don't see enough of a need

The main motivation for this - algorithms:

  1. There are things, that close to impossible to do with projections.
     // Data has following structure
     // | Header (16 bytes) | Payload(128byte) | ....
     // Seeking payload with specific header.
    using Iterator = iterator_t< std::vector<char> >;
    std::optional<Iterator> lookup(
       const std::vector<char>& raw_data, const std::vector<char>& required_header)
    {
       auto chunks  = raw_data | view::sliding(16+128);
       auto headers = 
         chunks 
         | view::forward_range
         | view::transform([](auto&& rng){ return rng | view::take(16); });
    
       auto founded = find_if(headers, [&](auto&& rng){ return equal(rng, required_header); });
       if (founded == headers.end()) return {};
       return {base<Iterator>(founded) + 16};   // return Itrerator pointing at payload start
    }
  2. You already have a view chain that do transformation , and you don't want to repeat yourself.
    Events = std::list<IEvent>;
    Events events;
    
    struct SyncPoint : IEvent{
        SyncFence& sync_fence();
        const SyncFence& sync_fence() const;
    };
    
    struct SyncFence{
        std::string text;
    }
    
    // This part already exists and in use.
    // -------------
    template<class T>
    auto get_only = 
       view::transform([](IEvent& e){ return dynamic_cast<T*>(&e); }) 
       | view::remove(nullptr)
       | view::indirect;
    
    auto get_sync_fences = 
       get_only<SyncPoint> 
       | view::transform([](SyncPoint& sync_point) -> SyncFence& {
            return sync_point.sync_fence();
        });
    // -------------
    
    // with base<>
    auto sync_fences = events | get_sync_fences ;
    auto founded = base<Events>(find(sync_fences, "red", &SyncFence::text));
    
    // with projection
    // It does not look much longer, but all that transformation chain
    // has substantial cognitive burden. While writing that transformer -
    // you'll forgot what exactly you wanted to search.
    auto founded = find_if(events, [](const IEvent& e) -> bool {
      const SyncPoint* p = dynamic_cast<const SyncPoint*>(&e);
      if (!p) return false;
      const SyncFence& fence = p->sync_fence();
      return (fence.text == "red");
    });
  3. You expose container adapter, not container itself:
    struct {
       auto elements(){
           return m_data | view::indirect;
       }
       void emplace_element(elements_iterator before, Element& data){
           auto data_iterator = base<Elements>(before);
           m_data.emplace(data_iterator, data);
       }
    private:
       using Elements= std::vector<Element*>;
       Elements m_elements;
    }

I never want to call .base() an arbitrary number of times.

The core part of this PR is only:

ranges::base<Iterator>(iter);

Everything else actually is auxiliary, and can be hided/removed.

tower1207y ago

added example 3.

ericniebler7y ago

This PR is now badly out of date. I never was able to convince myself that this problem justifies any solution, or that this solution is it. Do you still want this, @tower120?

tower1207y ago

My last line of defense is "ranges::base = better projections for lookup algorithms".

That's probably main use-case for ranges::base(). Previously I thought that ranges::base() can be used inside mutable algorithms/actions (like actions::remove( rng1, view::not_unique)), but abandoned that idea.

Important property of projection is that it can't skip elements, nor change their order. While view can.

using Pair =std::pair<int, int>; 
std::vector<Pair> pairs;

auto iter1 = ranges::find(pairs | view::partition(256) | view::each_range(view::take(4)).base(pairs);

You can't do that at all with projections, and you can't do that efficiently with predicates. And you definitely can't have that elegant solution with manual loop.

N.B.

auto each_range = [](auto view){
   // view::forward_iter = view::iota(view.begin(), view.end())    kinda...
   return view::forward_iter | view::transform([](auto&& rng){ return *rng | view; });
};

// view::partition = given a range and N value, return range of ranges:
// N 2
// In   1 2 3 4 5 6 7 8 9
// Out  12|34|56|78|9

I still think this is useful. But if you think it has too narrow application, close it.

UPDATE 1. Example updated, I mistakenly used wrong view.