6

For some background of why I am asking this question here is an example. In python the method chain chains an arbitrary number of ranges together and makes them into one without making copies. Here is a link in case you don't understand it. I decided I would implement chain in c++ using variadic templates. As far as I can tell the only way to make an iterator for chain that will successfully go to the next container is for each iterator to to know about the end of the container (I thought of a sort of hack in where when != is called against the end it will know to go to the next container, but the first way seemed easier and safer and more versatile).

My question is if there is anything inherently wrong with an iterator knowing about its own end, my code is in c++ but this can be language agnostic since many languages have iterators.

#ifndef CHAIN_HPP
#define CHAIN_HPP

#include "iterator_range.hpp"

namespace iter {
   template <typename ... Containers>
       struct chain_iter;
   template <typename Container>
       struct chain_iter<Container> {

        private:
           using Iterator = decltype(((Container*)nullptr)->begin());
           Iterator begin;
           const Iterator end;//never really used but kept it for consistency

        public:
           chain_iter(Container & container, bool is_end=false) :
               begin(container.begin()),end(container.end()) {
                   if(is_end) begin = container.end();
           }
           chain_iter & operator++()
           {
               ++begin;
               return *this;
           }
           auto operator*()->decltype(*begin)
           {
               return *begin;
           }
           bool operator!=(const chain_iter & rhs) const{
               return this->begin != rhs.begin;
           }
       };
   template <typename Container, typename ... Containers>
       struct chain_iter<Container,Containers...>
       {

        private:
           using Iterator = decltype(((Container*)nullptr)->begin());
           Iterator begin;
           const Iterator end;
           bool end_reached = false;
           chain_iter<Containers...> next_iter;

        public:
           chain_iter(Container & container, Containers& ... rest, bool is_end=false) :
               begin(container.begin()),
               end(container.end()),
               next_iter(rest...,is_end) {
                   if(is_end)
                       begin = container.end();
               }
           chain_iter & operator++()
           {
               if (begin == end) {
                   ++next_iter;
               }
               else {
                   ++begin;
               }
               return *this;               
           }
           auto operator*()->decltype(*begin)
           {
               if (begin == end) {
                   return *next_iter;
               }
               else {
                   return *begin;
               }
           }   
           bool operator !=(const chain_iter & rhs) const {
               if (begin == end) {
                   return this->next_iter != rhs.next_iter;
               }
               else
                   return this->begin != rhs.begin;
           }
        };
   template <typename ... Containers>
       iterator_range<chain_iter<Containers...>> chain(Containers& ... containers)
       {
           auto begin = 
               chain_iter<Containers...>(containers...);
           auto end =
               chain_iter<Containers...>(containers...,true);
           return 
               iterator_range<chain_iter<Containers...>>(begin,end);
       }
}

#endif //CHAIN_HPP
14
  • You can pass pairs of iterators, when the first is equal to the second you switch to the next pair. That's how the STL works. Commented Sep 24, 2013 at 2:23
  • @jozefg well I already have it working, but can you explain your solution more clearly, where are you passing the iterators to, just so you know my chain works identically to python's
    – aaronman
    Commented Sep 24, 2013 at 2:33
  • 1
    If you look through the STL, you traditionally pass iterators in pairs, and you iterate until the first is equal to the second. Eg passing the results of std::begin and std::end. This works for things like iostreams or other datastructures that might not be able to tell when they've reached the end, you compare to a sentinal value. Commented Sep 24, 2013 at 2:37
  • @jozefg maybe I didn't explain it perfectly, chain takes advantage of the new range based for loop in c++ like so for(auto i : chain(vec1,vec1,vec3){}, I know how the stdlib functions work but this is different and I don't think I can use your solution
    – aaronman
    Commented Sep 24, 2013 at 2:40
  • 1
    Firstly, I'd expect a file and a vector of strings to have an interchangeable iterator in this context. Secondly if you don't want to pass standalone iterators, then I'm afraid I don't see your dilemna. Most C++ code simply uses std::end and increments the iterator till you hit the result. That's the idiomatic way of doing it, alternatively, have nested for-eachs in count Commented Sep 24, 2013 at 3:00

3 Answers 3

6

Been there, got burned. Creating things that look like iterator but have different or extra requirements will lead to a mess. Basically many ranges are not copyable or at least not cheaply so, but that's what one normally expects of an iterator (it is a requirement of the iterator concept).

You should not have iterators that know of their own end. But chaining works with ranges. There are two ways to define ranges:

  • As forward-iterable "containers", which you can make of simple pair of iterators. This is a C++ way (and Boost.Range1 has some useful utilities for these), but sometimes it is quite a bit of extra work to make various objects that provide sequences fit the interface.

  • Define your interface for "generators". It will probably be similar to the python one, but since exceptions are less convenient in C++ than python, it will probably have different method of detecting end. I settled for following interface for my own needs2:

    template <typename T> concept Generator {
        bool valid();
        void next();
        T get();
    };
    

    where the iteration looks like:

    while(g.valid()) {
        auto item = g.get();
        do_anything_with(item);
        g.next();
    }
    

    the generator conceptually starts on first item in the sequence, but may only be accessed after valid is called. I found this allows distributing the hard work between constructor, valid and next as is fit for each case and it can be easily wrapped in iterator similarly to how istream_iterator is done. Other variations of the interface are possible including following the istream one (but it has disadvantage that it returns default element when the iteration fails).

Basically you should probably combine the approaches. If you use the later concept, you can adapt any such implementation to fit the (quite complex) Range concept from Boost.Range e.g. using "mixin" and Curiously Recurring Template Pattern. Something like:

template <typename GeneratorT, typename ValueT>
class GeneratorIterator :
    boost::iterator_facade<GeneratorT, ValueT, boost::single_pass_traversal_tag> {
    GeneratorT *g;
    GeneratorIterator() : g() {}
    GeneratorIterator(GeneratorT *g) g(g) {}
    ValueT &dereference() {
        if(!g || !g.valid())
            throw std::runtime_error("...");
        return g->get();
    }
    bool equal(GeneratorIterator const &l) {
        return g == l.g || ((!g || !g.valid()) && (!l.g || !l.g.valid()));
    }
    void increment() {
        if(g)
            g.next();
    }
}

template <typename GeneratorT, typename ValueT>
class GeneratorFacade {
  public:
    typedef GeneratorIterator<GeneratorT, ValueT> iterator;
    typedef GeneratorIterator<GeneratorT, ValueT> const_iterator;
    const_iterator begin() const {
        return const_iterator(this);
    }
    const_iterator end() const {
        return const_iterator();
    }
}

The advantage of the indirection is that the ranges now don't have to be copyable at all or not cheaply while the iterator is just a pointer and therefore is cheaply copyable as required. And defining generators is simple and easy to understand while they still end up conforming to the hairy standard C++ interface.

(Disclaimer: I wrote it off top of my head, not tested)


1 Boost.Range includes concatenating ranges. Don't reinvent the wheel and reuse or at least inspire yourself.

2 The Iterators Must Go talk linked in Ylisar's answer comes up with the same interface, just different names. Note that many languages combine the next/popFront and valid/empty to one next that returns a boolean, but that approach is much more difficult to wrap in iterators and conceptually somewhat more complex, because then the iterators start out in special "uninitialized" state.

5
  • There is nothing wrong with your answer but the point is that c++ range based for loops expect an object that has begin and end functions which is why my object may function like a range but in the end has to return two iterators
    – aaronman
    Commented Sep 24, 2013 at 17:03
  • @aaronman: It's easy to wrap the "generator" in proper iterators and provide appropriate begin and end functions for range-based for.
    – Jan Hudec
    Commented Sep 25, 2013 at 5:52
  • I mean that's basically what I ended up doing, IDK if you looked at the code, the first iter is the generator and end is essentially a dummy iter
    – aaronman
    Commented Sep 25, 2013 at 6:14
  • @aaronman: There is a difference whether the iterator is the generator and when the iterator wraps the generator. Iterators are expected to be cheaply copyable and the generator does not have to be copyable at all (like istream is not). So it's better to create a generator with separate API and wrap it in iterator (similar to how istream_iter wraps istream) than to implement the generator with iterator API.
    – Jan Hudec
    Commented Sep 25, 2013 at 6:29
  • @aaronman: I've added (boost-based, it's quite a lot of typing without boost) example how to adapt the simple generator interface to iterators and boost.range.
    – Jan Hudec
    Commented Sep 25, 2013 at 6:54
1

Iterators is actually considered to be more or less a design miss which is still in mostly because of legacy and slow adoption of ranges, which is a much more powerful concept ( which is what your case more or less is ). Take a look at http://www.slideshare.net/rawwell/iteratorsmustgo and the boost range library.

1
  • 3
    "Iterators suck" does not answer the question.
    – Philipp
    Commented Nov 6, 2013 at 16:36
1

So after some advice from @jozefg and research I learned that other languages (obviously not c++) have iterators with a concept of their own ends, including python. So I have decided that it is ok to have the self stopping iterators and am keeping it in my code.

1
  • 3
    Those are not iterators. They are "generators" or "ranges". C++ is trying to introduce those as well. See the links in other answers.
    – Jan Hudec
    Commented Sep 24, 2013 at 14:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.