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
Code
#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).