28 Iteratoren |
Dieses Kapitel behandelt die folgenden Themen:
Iterator-Kategorien
Typinformation mit Traits
Abstand und Bewegen von Iteratoren
Zugriff auf Anfang und Ende
Iteratoren zum Einfügen
Stream-Iteratoren
Iteratoren werden in Abschnitt 10.2 besprochen und an Beispielen gezeigt. Hier geht es um die vordefinierten Iteratortypen im Header <iterator> der C++-Standardbibliothek, die wie die Standardcontainer als Templates realisiert sind und über traits-Klassen bestimmte öffentliche Typnamen zur Verfügung stellen. Mehr zu Traits lesen Sie in Abschnitt 13.5. Natürlich könnten Typnamen auch direkt von einer Iteratorklasse veröffentlicht werden, aber man geht einen anderen Weg, weil die Algorithmen der C++-Standardbibliothek nicht nur auf Containern, die Typnamen bereitstellen, sondern auch auf einfachen C-Arrays arbeiten können sollen. Die damit arbeitenden Iteratoren sind aber nichts anderes als Zeiger, möglicherweise auf Grunddatentypen wie int. Ein Iterator des Typs int* kann sicher keine Typnamen zur Verfügung stellen. Aus diesem Grund gibt es eine Spezialisierung der traits-Klassen für Zeiger.
template<class Iterator> struct iterator_traits { using iterator_category = typename Iterator::iterator_category; using difference_type = typename Iterator::difference_type; using value_type = typename Iterator::value_type; using pointer = typename Iterator::pointer; using reference = typename Iterator::reference; };
Die Typen müssen natürlich für Iterator existieren. Damit ein Algorithmus, der mit Zeigern arbeitet, die üblichen Typnamen verwenden kann, wird das obige Template für Zeiger spezialisiert:
template<class T> struct iterator_traits<T*> { using iterator_concept = contiguous_iterator_tag; using iterator_category = random_access_iterator_tag; using value_type = remove_cv_t<T>; // = T ohne const und volatile using difference_type = ptrdiff_t; using pointer = T*; using reference = T&; };
Die Iterator-Kategorie wird im nächsten Abschnitt erläutert, auch konkrete Anwendungsbeispiele für Traits sind dort zu finden. Es gibt Abkürzungen, um Schreibarbeit zu sparen:
Typ |
Abkürzung |
iterator_traits<I>::difference_type |
iter_difference_t<I> |
iterator_traits<I>::value_type |
iter_value_t<I> |
iterator_traits<I>::reference |
iter_reference_t<I> |
28.1 | Iterator-Kategorien |
Es gibt verschiedene Kategorien von Iteratoren in einer hierarchischen Anordnung.
Input-Iterator: Ein Input-Iterator ist zum sequenziellen Lesen von Daten gedacht, zum Beispiel aus einem Container oder aus einer Datei. Ein Zurückspringen an eine schon gelesene Stelle ist nicht möglich (der ---Operator ist nicht definiert).
Output-Iterator: Ein Output-Iterator kann sequenziell in einen Container oder in eine Datei schreiben, wobei der Dereferenzierungsoperator verwendet wird. Beispiel:
Forward-Iterator: Wie Input- und Output-Iterator kann der Forward-Iterator sich vorwärts bewegen. Im Unterschied zu den vorgenannten Iteratoren können jedoch Werte des Iterators gespeichert werden, um ein Element des Containers wiederzufinden. Damit ist ein mehrfacher Durchlauf in eine Richtung möglich, zum Beispiel durch eine einfach verkettete Liste, wenn man sich den Anfang gemerkt hat.
Bidirectional-Iterator: Ein Bidirectional-Iterator kann all das, was ein Forward-Iterator kann. Darüber hinaus kann er noch mit dem ---Operator rückwärts gehen, sodass er zum Beispiel für eine doppelt verkettete Liste geeignet ist.
Random-Access-Iterator: Ein Random-Access-Iterator kann alles, was ein Bidirectional-Iterator kann. Zusätzlich ist ein wahlfreier Zugriff möglich, wie er für einen Vektor benötigt wird. Der wahlfreie Zugriff wird durch den Indexoperator operator[]() realisiert.
Contiguous-Iterator: Ein Contiguous-Iterator ist eine Verfeinerung des Random-Access-Iterators. Für einen Contiguous-Iterator I gilt, dass zwei aufeinanderfolgende Elemente *I und *(++I) auch im Speicher direkt aufeinanderfolgen.
Tabelle 28.2 zeigt eine Übersicht über mögliche Operationen einer Kategorie.
Operation |
Input |
Output |
Forward |
Bidirectional |
Random Access / Contiguous |
= |
• |
• |
• |
• |
|
== |
• |
• |
• |
• |
|
!= |
• |
• |
• |
• |
|
* |
1) |
2) |
• |
• |
• |
-> |
• |
• |
• |
• |
|
++ |
• |
• |
• |
• |
• |
-- |
• |
• |
|||
[ ] |
3) |
||||
arithmetisch |
4) |
||||
relational |
5) |
||||
1) Dereferenzierung ist nur lesend möglich. |
|||||
2) Dereferenzierung ist nur auf der linken Seite einer Zuweisung möglich. |
|||||
3) iter[n] bedeutet *(iter+n) für einen Iterator iter. |
|||||
4) + += - -= in Analogie zur Zeigerarithmetik |
|||||
5) < > <= >= relationale Operatoren |
Um einen Iterator mit einer Marke (englisch tag) zu versehen, gibt es Markierungsklassen (Listing 28.3). Ihre Anwendung folgt im nächsten Abschnitt.
struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {}; struct bidirectional_iterator_tag : public forward_iterator_tag {}; struct random_access_iterator_tag : public bidirectional_iterator_tag {}; struct contiguous_iterator_tag : public random_access_iterator_tag {};
28.1.1 | Anwendung von Traits |
In diesem Abschnitt wird zuerst gezeigt, wie mithilfe von Traits der Typ bestimmt und wie abhängig vom Typ der passende Algorithmus ausgewählt werden kann. Am Ende wird eine Alternative mit Concepts vorgeschlagen. Die Vorgehensweise, die richtige Methode mithilfe von Iterator-Tags zu ermitteln, heißt auf englisch »Tag Dispatching«. Der Compiler wählt im folgenden Beispiel dazu unter überladenen Funktionen aus. Deren Parameter, die Iteratorkategorie, wird aus dem Iterator mithilfe der Funktion getIteratortyp() ermittelt:
#include <fstream> #include <iostream> #include <iterator> #include <string> #include <vector> // Funktions-Template zur Ermittlung der Kategorie des Iterators template <class Iterator> [[nodiscard]] auto getIteratortyp(const Iterator&) { typename std::iterator_traits<Iterator>::iterator_category kategorie; return kategorie; } // überladene Funktionen void welcherIterator(const std::input_iterator_tag&) { std::cout << "Input-Iterator\n"; } void welcherIterator(const std::output_iterator_tag&) { std::cout << "Output-Iterator\n"; } void welcherIterator(const std::forward_iterator_tag&) { std::cout << "Forward-Iterator\n"; } void welcherIterator(const std::random_access_iterator_tag&) { std::cout << "Random-Access-Iterator\n"; } using namespace std; int main() // Anwendung { // Bei Grunddatentypen (hier: ein Zeiger) wird das partiell spezialisierte // iterator_traits<T*>- Template von Seite 892 benutzt. int* ip = nullptr; // Random-Access-Iterator // Anzeige des Iteratortyps welcherIterator(getIteratortyp(ip)); welcherIterator(iterator_traits<int*>::iterator_category()); // Definition eines Datei-Objekts zum Lesen (eine tatsächliche Datei // ist nicht erforderlich, es geht nur um den Typ) ifstream Source; istream_iterator<string> IPos(Source); // Ein istream_iterator ist ein Input-Iterator. // Anzeige des Iteratortyps welcherIterator(getIteratortyp(IPos)); // oder: welcherIterator( iterator_traits<istream_iterator<string>>::iterator_category()); ofstream ziel; // Definition eines Datei-Objekts zum Schreiben ostream_iterator<string> OPos(ziel); // Ein ostream_iterator ist ein Output-Iterator. // Anzeige des Iteratortyps welcherIterator(getIteratortyp(OPos)); // oder: welcherIterator( iterator_traits<ostream_iterator<string>>::iterator_category()); vector<int> v(10); // Anzeige des Iteratortyps welcherIterator(getIteratortyp(v.begin())); // oder ein anderer Iterator als begin() welcherIterator(iterator_traits<vector<int>::iterator>::iterator_category()); }
Im zweiten Beispiel wird der am besten passende Algorithmus automatisch zur Compilierzeit ausgewählt. Es soll das mittlere Element eines Containers zurückgegeben werden. Bei einer Liste müssen dazu N/2 Elemente abgeklappert werden, es ist also eine Schleife notwendig. Bei einem Vektor nimmt man einfach das Element [N/2]. Dabei werden einer Funktion mittleresElement(Iterator anfang, int n) der Anfang des Containers als Iterator und die Anzahl der Elemente mitgeteilt. Aus dem Typ des Iterators wird die passende überladene aufzurufende Funktion ermittelt:
#include <iostream> #include <iterator> #include <list> #include <vector> template <class Iterator> // erste überladene Funktion [[nodiscard]] auto holeMittleres(Iterator anfang, int n, std::input_iterator_tag) { for (int i = 0; i < n / 2; ++i) { // bis zur Mitte laufen ++anfang; } return *anfang; } // Der bidirektionale Iterator lässt keine wahlfreien Zugriffe und damit // keine Iteratorarithmetik zu. Zur Bewegung sind nur die Operatoren // ++ und -- erlaubt. Ein Random-Access-Iterator erlaubt // Arithmetik, sodass die Implementation für diesen Fall etwas einfacher ist: template <class Iterator> // zweite überladene Funktion [[nodiscard]] auto holeMittleres(Iterator anfang, int n, std::random_access_iterator_tag) { return *(anfang + n / 2); // Arithmetik } // Aufrufende Funktion. // Diese Funktion ruft nun die korrespondierende überladene Variante auf, // wobei die Auswahl zur Compilierzeit durch den Parameter kategorie geschieht. template <class Iterator, typename Int> [[nodiscard]] auto mittleresElement(Iterator anfang, Int n) { typename std::iterator_traits<Iterator>::iterator_category kategorie; return holeMittleres(anfang, n, kategorie); } int main() // Hauptprogramm { const std::list<int> liste{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // list const std::vector<int> vec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // vector // Aufruf der ersten Implementierung für die Liste std::cout << mittleresElement(liste.begin(), liste.size()) << ’\n’; // Aufruf der zweiten Implementierung für den Vektor std::cout << mittleresElement(vec.begin(), vec.size()) << ’\n’; }
Tipp
Concepts bieten eine einfachere Möglichkeit, den richtigen Algorithmus auszuwählen.
Die Alternative mit dem Concept random_access_iterator sehen Sie in dem Listing 28.6. Von den zwei Funktions-Templates wird das spezialisiertere ausgewählt, wenn es passt.
#include <iostream> #include <list> #include <iterator> #include <vector> template <class Container> // passt für Vektor und Liste [[nodiscard]] auto mittleresElement(const Container& cont) { auto iter {cont.begin()}; auto anzahl {0u}; while(anzahl++ < cont.size()/2) { // Alternative advance(), siehe Text ++iter; } return *iter; } // passt für Vektor, aber nicht für Liste [[nodiscard]] auto mittleresElement( const std::random_access_iterator auto& cont) { return cont[cont.size()/2]; } int main() // Hauptprogramm { std::list<int> liste{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; std::vector<int> vec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // Aufruf der ersten Implementierung für die Liste std::cout << mittleresElement(liste) << ’\n’; // Aufruf der zweiten Implementierung für den Vektor std::cout << mittleresElement(vec) << ’\n’; }
Anstelle der Zählschleife könnte man advance(iter, cont.size()/2) schreiben, siehe nächster Abschnitt. Weil advance() gegebenenfalls Iteratorarithmetik verwendet, wäre das erste Funktions-Template für Vektoren praktisch genauso schnell wie das zweite.
28.2 | Abstand und Bewegen |
Weil nur Random-Access-Iteratoren die Operationen + und - erlauben, gibt es die Funktionen distance() zum Ermitteln eines Iteratorabstands und advance(), next() und prev() zum Weiterschalten. Diese Funktionen benutzen intern + und - für Random-Access-Iteratoren und in anderen Fällen ++ bzw. --. Dabei ist der unten zur Abkürzung verwendete IntType der zum Iterator passende Datentyp für Differenzen (typisch int oder long int), also iterator_traits<InputIterator>::difference_type für einen InputIterator.
template<class InputIterator>
constexpr IntType distance(InputIterator first, InputIterator last);
gibt den Abstand zwischen zwei Iteratoren zurück. Dabei muss last von first aus erreichbar sein.
template<class InputIterator, class Distance>
constexpr void advance(InputIterator& i, Distance n);
schaltet i um n Positionen vor bzw. zurück, falls n < 0 ist.
template <class InputIterator>
constexpr InputIterator next(InputIterator x, IntType n = 1);
entspricht advance(x, n), gibt aber x zurück.
template <class BidirectionalIterator>
constexpr BidirectionalIterator prev(BidirectionalIterator x, IntType n = 1);
entspricht advance(x, -n), gibt aber x zurück.
28.3 | Zugriff auf Anfang und Ende |
Für den Zugriff auf Anfang und Ende eines Bereichs stellt C++ die Funktionen der linken Spalte der Tabelle 28.3 zur Verfügung. Die Funktionen geben einen Iterator zurück, dessen Typ vom Objekt abhängt, auf das die Funktion angewendet wird. Die Tabelle zeigt die Rückgabewerte für drei verschiedene Objekttypen.
Funktion |
Container |
Initialisierungsliste |
C-Array |
std::begin(x) |
x.begin() |
x.begin() |
x |
std::end(x) |
x.end() |
x.end() |
x + N |
std::cbegin(x) |
std::begin(x)) |
std::begin(x) |
std::begin(x) |
std::cend(x) |
std::end(x) |
std::end(x) |
std::end(x) |
std::rbegin(x) |
x.rbegin() |
RI<const T*>(x.end()) |
RI<T*>(x+N) |
std::rend(x) |
x.rend() |
RI<const T*>(x.begin()) |
RI<T*>(x) |
std::crbegin(x) |
std::rbegin(x) |
std::rbegin(x) |
std::rbegin(x) |
std::crend(x) |
std::rend(x) |
std::rend(x) |
std::rend(x) |
Abkürzungen siehe Text.
T ist der Typ der Elemente des Containers, der Initialisierungsliste oder des C-Arrays.
x ist das jeweils übergebene Objekt oder eine const-Referenz auf das Objekt. Im Fall des C-Arrays ist x vom Typ const T*, ansonsten vom Typ X oder const X&.
N ist die Anzahl der Arrayelemente, das heißt, N = sizeof(x)/sizeof(x[0]). Die Funktionen sind nur dann möglich, wenn das Array vorher im selben Sichtbarkeitsbereich definiert wurde.
RI bedeutet std::reverse_iterator. Die Abkürzung wurde hier nur eingeführt, damit die Tabelle nicht zu breit wird. Reverse-Iteratoren erlauben das Durchlaufen eines Bereichs vom Ende zum Anfang. Sie werden unten in Abschnitt 28.3.1 behandelt.
Bitte beachten Sie, dass die const-Eigenschaft im Rückgabetyp der jeweiligen Funktion berücksichtigt wird. Beispiel cbegin(x): Es gibt nur die Funktion mit einem const X&-Parameter, während es von den nicht mit c beginnenden Funktionen beide Varianten gibt, also etwa begin(X&) und begin(const X&). In cbegin(x) steht letztlich nur die Anweisung return begin(x);. Wegen des const X&-Parameters wird die zweite Variante, also begin(const X&), aufgerufen und der von dieser Funktion gelieferte const-Iterator zurückgegeben.
Falls x eine Initialisierungsliste ist, könnte die Verwendung der Funktionen wie folgt aussehen:
#include <initializer_list> #include <iostream> #include <iterator> void f(std::initializer_list<int> liste) { auto anfang = std::begin(liste); while (anfang != std::end(liste)) { std::cout << *anfang++ << ’\n’; } } int main() { f({9, 8, 7}); }
28.3.1 | Reverse-Iteratoren |
Ein Reverse-Iterator ist bei einem bidirektionalen Iterator immer möglich. Ein Reverse-Iterator durchläuft einen Container rückwärts mit der ++-Operation. Beginn und Ende eines Containers für Reverse-Iteratoren werden durch rbegin() und rend() markiert. Dabei verweist rbegin() auf das letzte Element des Containers und rend() auf die (ggf. fiktive) Position vor dem ersten Element. Einige Container stellen Reverse-Iteratoren zur Verfügung. Sie werden mit der vordefinierten Klasse
realisiert. Ein Objekt dieser Klasse wird mit einem bidirektionalen oder einem Random-Access-Iterator initialisiert, entsprechend dem Typ des Template-Parameters. Ein Reverse-Iterator arbeitet intern mit diesem Iterator und legt eine Hülle (englisch wrapper) mit bestimmten zusätzlichen Operationen um ihn herum. Für einen existierenden Iterator wird eine neue Schnittstelle geschaffen, um sich verschiedenen Gegebenheiten anpassen (englisch to adapt) zu können. Deshalb werden Klassen, die eine Klasse in eine andere umwandeln, Adapter genannt. Ein Beispiel sehen Sie unten. Zu den in der Tabelle 28.2 auf Seite 893 angegebenen Operationen für bidirektionale oder Random-Access-Iteratoren gibt es die Methode base(), die den gekapselten Iterator zurückgibt.
#include <iostream> #include <iterator> #include <vector> using namespace std; int main() { vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; cout << "reverse_iterator:\n"; auto revIter = v.rbegin(); // Alternativen: // reverse_iterator<vector<int>::iterator> revIter(v.rbegin()); // vector<int>::reverse_iterator revIter(v.rbegin()); // Zahlen verdoppelt in umgekehrter Folge ausgeben while (revIter != v.rend()) { *revIter *= 2; // Wert über den Iterator ändern cout << *revIter++ << ’˽’; } cout << ’\n’; // Falls Werte nicht geändert werden sollen, kann der von der Klasse vector (und // anderen) bereitgestellte Typ const_reverse_iterator verwendet werden. cout << "const_reverse_iterator:\n"; auto constRevIter = v.crbegin(); // = vector<int>::const_reverse_iterator while (constRevIter != v.crend()) { cout << *constRevIter++ << ’˽’; } cout << ’\n’; }
28.4 | Insert-Iteratoren |
Mit normalen Iteratoren bewirkt der Code
das Kopieren des Bereichs [first, last) an die Stelle result, wobei der vorherige Inhalt an der Stelle überschrieben wird. Derselbe Code bewirkt jedoch das Einfügen in einen Container, wenn result ein Insert-Iterator ist. Je nach gewünschter Position zum Einfügen gibt es drei Varianten. Von denen ist eine ein sogenannter front_insert_iterator, an dem vorab erklärt wird, wie ein Insert-Iterator funktioniert. Die Schreibweise wie oben (*result++ = *first++;) soll beibehalten werden. Ein front_insert_iterator setzt voraus, dass die Methode push_front() zur Verfügung steht. Ein Anwendungsbeispiel:
list<int> dieListe {1, 2, 3, 4, 5}; front_insert_iterator<list<int>> frontInsertIterator(dieListe); int i = 1; while (i < 5) { // 4 Zahlen vorne einfügen *frontInsertIterator++ = i++; // mit den Operationen *, ++, = }
Der Konstruktor des Iterators merkt sich den Container. Letztlich entscheidend ist die Zuweisung mit i++ auf der rechten Seite, denn dadurch ändert sich der Inhalt des Containers. Das kann so gelöst werden, dass operator=(int) die Methode push_front() des Containers aufruft. Was aber ist mit operator*() und operator++()? Die tun am besten gar nichts. Sie können das Beispiel in cppbuch/k28/finsert.cpp so modifizieren, dass Sie überall front_insert_iterator durch meinFrontInsertIterator ersetzen – das Programm funktioniert genauso gut, wenn Sie vorab meinFrontInsertIterator wie folgt definieren:
template <class Container> struct meinFrontInsertIterator { meinFrontInsertIterator(Container& cont) : container(cont) { } meinFrontInsertIterator& operator=( const typename Container::value_type& value) { container.push_front(value); return *this; } meinFrontInsertIterator& operator*() { return *this; } meinFrontInsertIterator& operator++() { return *this; } meinFrontInsertIterator operator++(int) { return *this; } Container& container; };
Die letzten drei Operatoren tun nichts, außer *this zurückzugeben.
1. front_insert_iterator (Beispiel siehe cppbuch/k28/finsert.cpp)
Dieser Insert-Iterator fügt etwas am Anfang eines Containers ein. Der Container muss die Methode push_front() zur Verfügung stellen. Ein Anwendungsbeispiel finden Sie oben.
front_inserter() ist eine Funktion, die einen front_insert_iterator zurückgibt. Ein Beispiel mit dem Standardalgorithmus copy() (siehe auch cppbuch/k28/finsert.cpp):
Die Reihenfolge dreht sich dabei um, weil erst das erste Element des Vektors am Anfang der Liste eingefügt wird, dann das zweite usw.
2. back_insert_iterator (Beispiel siehe cppbuch/k28/binsert.cpp)
Dieser Insert-Iterator fügt etwas am Ende eines Containers ein. Der Container muss die Methode push_back() zur Verfügung stellen. Anwendungsbeispiel:
back_inserter() ist eine Funktion, die einen back_insert_iterator zurückgibt. Ein Beispiel mit dem Standardalgorithmus copy():
3. insert_iterator (Beispiel siehe cppbuch/k28/insert.cpp)
Dieser Insert-Iterator fügt etwas an einer ausgewählten Position in den Container ein. Der Container muss die Methode insert() zur Verfügung stellen. Die Anwendung ist ähnlich wie vorher, nur dass dem Insert-Iterator die gewünschte Einfügeposition als Iterator mitgegeben werden muss:
inserter() ist eine Funktion, die einen insert_iterator zurückgibt. Die Anwendung wird dadurch manchmal einfacher. Ein Beispiel mit dem Standardalgorithmus copy() von Seite 800:
Übung
28.1 Wenn Sie in cppbuch/k28/finsert.cpp die Zeile *frontInsertIterator++ = i++; durch frontInsertIterator = i++; ersetzen, ändert sich das Verhalten des Programms nicht. Wie erklären Sie das?
28.5 | Stream-Iteratoren |
Stream-Iteratoren dienen zum sequenziellen Lesen und Schreiben von Strömen mit den bekannten Operatoren << und >>. Der Istream-Iterator ist ein Input-Iterator, der Ostream-Iterator ein Output-Iterator. Beispiele:
ifstream quelle("Datei.txt"); istream_iterator<string> pos(quelle); istream_iterator<string> ende; while (pos != ende) { cout << *pos++ << ’\n’; }
Die Dereferenzierung von pos in der Schleife gibt den gelesenen Wert zurück. Es lassen sich eigene Istream-Iteratoren mit besonderen Eigenschaften schreiben. Dem Konstruktor eines Ostream-Iterators kann wahlweise eine Zeichenkette zur Trennung von Elementen mitgegeben werden.
ifstream quelle("Datei.txt"); istream_iterator<string> iPos(quelle); istream_iterator<string> ende; ofstream ziel("Ergebnis.txt"); // Hier wird bei der Ausgabe jede Zeile mit einem * versehen: ostream_iterator<string> oPos(ziel, "*\n"); while (iPos != ende) { *oPos++ = *iPos++; }
Im Folgenden wird als konkretes Beispiel ein IStream-Iterator gezeigt, der die Bezeichner (englisch identifier) einer Datei liest. Der Iterator hat alle benötigten Methoden, insbesondere operator*(), operator++() und operator++(int);
#ifndef IDISTREAMITERATOR_H #define IDISTREAMITERATOR_H #include <cctype> #include <fstream> class IdIstreamIterator { public: IdIstreamIterator() = default; explicit IdIstreamIterator(std::ifstream& ifstr) : is(&ifstr) { } [[nodiscard]] auto operator*() const { return bezeichner; } IdIstreamIterator& operator++() { bezeichner = ""; char c = ’\0’; // Wortanfang finden while (is->good() && !(isalpha(static_cast<unsigned char>(c)) || ’_’ == c)) { is->get(c); } bezeichner += c; // Wenn der Anfang gefunden wurde, werden alle folgenden Unterstriche und alphanumeri- // schen Zeichen gesammelt. Whitespace oder Sonderzeichen beenden den Leseprozess. while (is->good() && (isalnum(static_cast<unsigned char>(c)) || ’_’ == c)) { is->get(c); if (isalnum(static_cast<unsigned char>(c)) || ’_’ == c) bezeichner += c; } if (!is->good()) { is = nullptr; } else { // Das letzte Zeichen gehört nicht zum Identifier und wird deshalb zurückgeschrieben. is->putback(c); } return *this; } [[nodiscard]] IdIstreamIterator operator++(int) { auto tmp = *this; ++*this; return tmp; } friend bool operator==(const IdIstreamIterator&, const IdIstreamIterator&); private: std::string bezeichner; std::istream* is {nullptr}; }; bool operator==(const IdIstreamIterator& i1, const IdIstreamIterator& i2) { return i1.is == i2.is; } #endif
Der operator!=() wird vom Compiler automatisch aus operator==() erzeugt. In der Anwendung wird ein IdIstreamIterator erzeugt, der mit einem ifstream initialisiert wird. Das folgende Programm gibt die Bezeichner auf der Konsole aus:
#include "IdIstreamIterator.h" #include <iostream> int main() { std::ifstream datei("main.cpp"); // Eingabedatei öffnen if(!datei) { std::cerr << "FEHLER\n"; return 1; } IdIstreamIterator iter(datei); IdIstreamIterator end; while (iter != end) { std::cout << *iter++ << ’\n’; // lesen, ausgeben und weiterschalten } }