Ein Iterator zur Außenwelt
Manchmal wollen wir gern Dinge mit der Standardbibliothek verbinden, die so erstmal nicht vorgesehen sind. Zu dem Zweck kann
ein selbst implementierter Iterator hilfreich sein. Im Beispiel hier soll ein OutputIterator die Werte, die in ihn
geschrieben werden, in eine grafische Oberfläche übertragen. Dafür muss er natürlich die Funktionalität eines
OutputIterator unterstützen und zusätzlich noch seine Fähigkeiten der Standardbibliothek bekanntgemacht werden.
Video
Code
|
|
Erklärung
Eine Menge Quelltext, der aber in verhältnismäßig übersichtliche Teile zerfällt. Die Zeilen 77 bis 86 und alle Qt-Header können wir eigentlich getrost ignorieren. Die sind bloß notwendig, um die grafische Oberfläche zu erzeugen (daher ist das direkte Kompilieren des Quellcodes heute auch nicht so einfach. Die Qt-Library muss nämlich noch angebunden werden.). Interessant ist lediglich, dass es in der Oberfläche ein QLabel gibt, welches Text anzeigen kann und in welches unser Iterator schreiben soll.
Herzstück des Programms ist das Template GuiIterator. Als Templateparamater erhält es den Datentyp, den das entsprechende
Iteratorobjekt schreiben soll. Ein OutputIterator muss nur relativ wenige Funktionen unterstützen: er muss inkrementiert
werden können (das interessanterweise sowohl mit Prä-, als auch Postfix-Inkrement) und er muss dereferenziert werden können,
wobei das Ergebnis der Dereferenzierung eine Zuweisung aus dem Datentyp, mit dem der Iterator verwendet werden soll,
unterstützen muss. Die Inkrements und Dereferenzierung stellt der GuiIterator bereit, indem er die entsprechenden
Operatoren implementiert. Interessant ist hier die Unterscheidung zwischen Präfix und Postfix: die Postfix-Variante hat
einen anonymen int-Parameter. Immer wenn wir ++i schreiben, ruft der Compiler i.operator++() auf. Schreiben wir
hingegen i++, dann wird daraus i.operator++(int) (wobei der übergebene Wert für den int unspezifiziert ist). In
unserem Fall muss der Iterator nicht hochzählen, weswegen beide Operatoren gleich sind: sie liefern einfach den Iterator
selbst zurück. Würden wir den Iterator intern verändern (bspw. auf das nächste zu schreibende Element weiterzählen), dann
müsste das Postfix-Inkrement den alten Wert (bspw. als temporäres Objekt) zurückliefern, wohingegen Präfix-Inkrement den
neuen Wert liefert. Dieser kleine, aber gewichtige Unterschied wird gern übersehen.
Die Dereferenzierung des Iterators würde normalerweise eine Referenz auf den zu schreibenden Wert liefern (bspw. eine
int-Variable), die dann zugewiesen werden kann. In unserem Fall ist das natürlich nicht so einfach: wir wollen ja den
Inhalt des QLabel anpassen, brauchen also etwas mehr Komplexität. Zu dem Zweck gibt es die interne (aber öffentliche)
Klasse Executor, die eine Zuweisung aus dem Datentyp des umgebenden Templates unterstützt und den Transfern in das Label
implementiert. Da diese Klasse eine nested class innerhalb des GuiIterator-Templates ist, ist sie selbst auch mit
parametrisiert. GuiIterator<int>::Executor ist also etwas anderes, als GuiIterator<std::string>::Executor. Wir bekommen
so ganz bequem Zugriff auf den Templateparameter der umgebenden Klasse und können sicherstellen, dass wir im operator=()
den passenden Parametertypen implementieren. Die eigentliche Implementierung ist dann relativ simpel: wir nutzen
std::ostringstream, eine Klasse, die das normale Output-Stream-Interface bíetet, aber intern das Ergebnis in einem String
speichert und wieder bereitstellt. Dort bauen wir dann den neuen Text des QLabel zusammen und schreiben ihn anschließend
zurück.
Ein letztes notwendiges Detail, damit die Standardbibliothek auch mit unserem Iterator spielen will, ist die Spezialisierung
des Templates std::iterator_traits für unseren Iterator. Mittels der iterator_traits bekommt die Standardbibliothek zur
Übersetzungszeit einige notwendige Informationen über unseren Iterator raus. In unserem Fall interessant ist nur der
typedef für iterator_category. Da wir den als Alias für output_iterator_tag anlegen, weiß die Standardbibliothek, dass
sie es mit einem OutputIterator zu tun hat. Alles andere ist dann nicht mehr interessant, da diese Typen nur für
InputIteratoren relevant wären.
Zuguterletzt können wir unseren Iterator im std::copy als Ziel angeben und die Werte werden wie gewünscht in der
grafischen Oberfläche landen.