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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
#include <Qt/qapplication.h>
#include <Qt/qmainwindow.h>
#include <Qt/qlabel.h>
#include <Qt/qgridlayout.h>
 
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <iterator>
#include <sstream>
 
template <typename DataT> class GuiIterator
{
public:
     
    GuiIterator(QLabel *l)
        : mExecutor(l)
    {
    }
     
    class Executor
    {
        QLabel *label;
         
    public:
        Executor(QLabel *l)
            : label(l)
        {
        }
         
        Executor &operator=(DataT const &value)
        {
            std::ostringstream s;
            s << label->text().toStdString();
            s << "\n";
            s << value;
            label->setText(s.str().c_str());
            return *this;
        }
    };
     
    GuiIterator &operator++()
    {
        return *this;
    }
     
    GuiIterator &operator++(int)
    {
        return *this;
    }
     
    Executor &operator*()
    {
        return mExecutor;
    }
 
private:
    Executor mExecutor;
};
 
namespace std
{
    template <typename T> class iterator_traits<GuiIterator<T> >
    {
    public:
        typedef void difference_type;
        typedef void value_type;
        typedef void pointer;
        typedef void reference;
        typedef output_iterator_tag iterator_category;
    };
}
 
int main(int argc, char *argv[])
{
    QApplication qapp(argc, argv);
    QMainWindow mw;
     
    QLabel label;
         
    QFont font;
    font.setPointSize(16);
    font.setBold(true);
    label.setFont(font);
    mw.setCentralWidget(&label);
         
    std::vector<int> v;
    v.push_back(2);
    v.push_back(7);
    v.push_back(4);
 
    std::copy(v.begin(), v.end(), GuiIterator<int>(&label));
     
    std::list<std::string> l;
    l.push_back("Eins");
    l.push_back("Zwo");
    l.push_back("Drei");
     
    std::copy(l.begin(), l.end(), GuiIterator<std::string>(&label));
     
    mw.show();
    qapp.exec();
}

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.