Iterieren über eigene Container

Wie kann die InfiniteSequence vom letzten Mal etwas konformer zur Standardbibliothek gemacht werden? Ganze einfach: man verpasst ihr passende Iteratoren, wie sie für die anderen Container auch vorgegeben sind. Der Einfachheit halber sind das hier mal nur ForwardIterator. Vom Entwurf des Containers her wären aber auch Bidirektional oder RandomAccess möglich.

Video

Quellcode

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <initializer_list>

template <typename T> class InfiniteSequence
{
  std::vector<T> mValues;
  
public:
  
  class const_iterator : public std::iterator<std::forward_iterator_tag, int>
  {
    int position;
    InfiniteSequence *seq;
    
  public:
    
    const_iterator(InfiniteSequence *s)
      : const_iterator(s, 0)
    {
    }
    
    const_iterator(InfiniteSequence *s, int p)
      : position(p), seq(s)
    {
    }
    
    const_iterator() : const_iterator(nullptr, 0) { }
    const_iterator(const_iterator const &other) = default;
    const_iterator &operator=(const_iterator const &other) = default;
    
    bool operator==(const_iterator const &other)
    {
      return position == other.position;
    }

    bool operator!=(const_iterator const &other)
    {
      return position != other.position;
    }
    
    T const &operator*() const
    {
      return seq->get(position);
    }
    
    iterator &operator++()
    {
      ++position;
      position %= seq->mValues.size();
      return *this;
    }
    
    iterator &operator++(int)
    {
      iterator copy {*this};
      ++position;
      position %= seq->mValues.size();
      return copy;
    }  
  };
  
  InfiniteSequence(T const &min, T const &max)
  {
    for (T i = min; i <= max; ++i) {
      mValues.push_back(i);
    }
  }
  
  InfiniteSequence(std::initializer_list<T> const &values)
  {
    for (auto i : values) {
      mValues.push_back(i);
    }
  }
  
  const_iterator begin()
  {
    return const_iterator(this);
  }
  
protected:
  
  T const &get(unsigned int pos) const
  {
    return mValues[pos % mValues.size()];
  }
};

int main()
{
  InfiniteSequence<int> inf { 1,6,3,2,9,10 };
  InfiniteSequence<double> inf2(1.4, 5.7);
  
  auto it = inf.begin();
  for (int i = 0; i < 20; ++i) {
    std::cerr << *it << " ";
    ++it;
  }
  std::cerr << std::endl;

  auto it2 = inf2.begin();
  for (int i = 0; i < 20; ++i) {
    std::cerr << *it2 << " ";
    ++it2;
  }
  std::cerr << std::endl;
  
  std::copy_n(inf.begin(), 30, std::ostream_iterator<int>(std::cerr, " "));
  std::cerr << std::endl;
}

Erklärung

Gegenüber der ursprünglichen InfiniteSequence wurde diese hier um zwei größere Sachen verändert: zum einen ist die Methode get() aus der öffentlichen Schnittstelle entfallen und dient nun stattdessen mit einem zusätzlichen Parameter dem wahlfreien Zugriff auf Elemente der Sequenz. Zum anderen gibt es eine nested class namens const_iterator, die den Iteratortyp der InfiniteSequence repräsentiert. const_iterator erfüllt die Anforderungen an einen ForwardIterator: es gibt einen Default- und Copy-Konstruktor, einen Copy-Assignment-Operator, Objekte des Iteratortyps können auf Gleichheit oder Ungleichheit verglichen werden (der operator!= fehlt übrigens in der Videoversion. Das ist ein Fehler, der so an sich erstmal nicht auffällt, da ich ihn nicht verwende), es gibt ein Prä- und Postfixinkrement und eine Dereferenzierung. Da das ganze ein konstanter Iterator ist (die zugrundliegende Sequenz ist ja auch nicht veränderlich) liefert operator* eine konstante Referenz auf den Wertetyp zurück. Soweit alles wie zu erwarten.

const_iterator als nested class zu realisieren hat einen Vorteil: die Klasse selbst muss kein Template sein. Sie ist als abhängiger Typ von InfiniteSequence automatisch mit parametrisiert. InfiniteSequence<int>::const_iterator und InfiniteSequence<double>::const_iterator sind also bereits unterschiedliche Typen. Ein extra Templateparameter für den Iterator ist unnötig.

Kleine Besonderheiten im Quelltext: zum einen die beiden Delegate Constructors in Zeile 21 und 30. Mit C++11 kommt die Möglichkeit hinzu, Konstruktoren der eigenen Klasse aus anderen Konstruktoren aufzurufen. Das ist immer dann ganz angenehm, wenn man gewisse Teile einer einheitlichen Initialisierung machen und dann in einigen Konstruktoren noch zusätzliche Aktionen durchführen möchte. In C++03 bedeutete das immer, dass man eine extra init-Funktion mit der einheitlichen Initialisierung implementieren musste, die jeder Konstruktor aufrufen konnte (und damit lassen sich einige Randfälle speziell bei der Initialisierung der Membervariablen der Klasse nicht abdecken). C++11 bietet hier jetzt die doch etwas bequemere Variante mit der Konstruktorweiterleitung. Dazu steht der entsprechende Aufruf des anderen Konstruktors einfach in der Initialisierung des aufrufenden Konstruktors. Hier im Beispiel wäre das nicht zwingend notwendig. Man könnte das Verhalten hier genauso mit einem Konstruktor und entsprechenden Default-Werten für die Parameter lösen. In anderen Fällen ist das nicht so einfach.

Als weiteres Beispiel ist im Quelltext die Implementierung des Postfixinkrements zu sehen. Der Unterschied zum Präfixinkrement: der Zustand des Iterators vor der Inkrementierung wird zurückgeliefert (im Beispiel vorher in der Variable copy gespeichert).