Streams (3) - Nutzerdefinierte Manipulatoren

Martin Kompf

Zusätzlich zu den Standard Stream-Manipulatoren kann der Entwickler eigene Manipulatoren definieren. Lohn der Mühe ist einfacher zu schreibender und besser zu verstehender Code.

In der letzen Artikelfolge wurden die in der iostream Library enthaltenen Manipulatoren vorgestellt. Das Design der IO-Streams erlaubt es nun dem Benutzer, zusätzlich eigene Manipulatoren zu definieren. Ein einfaches Beispiel wäre die Definition eines Manipulators hr, der in einen Outputstream eingefügt, die Ausgabe einer horizontalen Linie bewirkt:

#include <iostream>
#include <iomanip>

using namespace std;

ostream& hr(ostream& os)
{
    os << endl;
    for (int i = 0; i < 72; ++i) os << '-';
    return os << endl;
}

//...
    
cout << hr << "Line 1" << hr << "Line 2" << hr << endl;

Manipulierte Eingabe

Interessant ist auch die Möglichkeit, eigene Manipulatoren für Inputstreams zu definieren. Wir erinnern uns an den aufgrund der vielen if-Konstrukte relativ unübersichtlichen Input Stream Operator für die point-Klasse aus dem ersten Streams-Artikel. Dieser Operator soll Eingaben der Form (12,5) aus der Eingabe lesen und daraus ein Objekt vom Typ point erzeugen. Wie schön wäre es doch, könnte man den Operator einfach so deklarieren:

istream& operator>> (istream &is, point &p)
{
    int x, y;
    is >> lparan >> x >> comma >> y >> rparan;

    if (is) p = point( x, y);
    return is;
}

Und tatsächlich, C++ erlaubt diese einfache Notation! Was nur noch fehlt, sind die nutzerdefinierten Manipulatoren lparan, rparan und comma, die die Zeichen linke Klammer, rechte Klammer beziehungsweise Komma aus der Eingabe lesen. Eine mögliche Kodierung für den comma Manipulator wäre:

istream& comma(istream& is)
{
    char c;
    if (is >> c && c != ',')
        is.clear(ios::badbit);
    return is;
}

Der Kode für die anderen beiden Manipulatoren lässt sich daraus leicht ableiten, das ganze schreit förmlich nach einer Implementierung als Funktionstemplate:

template <char sep>
istream& separator(istream& is)
{
    char c;
    if (is >> c && c != sep)
        is.clear(ios::badbit);
    return is;
}

Für jeden verwendeten Manipulator muss dieses Template nun noch instanziert werden:

istream& (*lparan)(istream&) = separator<'('>;
istream& (*rparan)(istream&) = separator<')'>;
istream& (*comma)(istream&) = separator<','>;

Argumente

Diese Vorgehensweise funktioniert gut - allerdings nur bei Manipulatoren, die keine Argumente verlangen. Beabsichtigt man hingegen, einen Manipulator wie pad(n) zu definieren, der in die Ausgabe n Füllzeichen einfügt, so ist ein Umweg nötig. Zunächst muss eine (universell verwendbare) Hilfsklasse definiert werden. Diese stellt insbesondere einen Output Stream Operator zur Verfügung:

class omani
{
    int i_;
    ostream& (*f_)(ostream&, int);
public:
    omani(ostream& (*f)(ostream&, int), int i) : f_(f), i_(i) {}
    friend ostream& operator<<( ostream& os, omani m)
        {return m.f_(os, m.i_);}
};

Die Membervarible f_ hält hierbei einen Zeiger auf die eigentliche Ausgabefunktion. Diese hier fügt die entsprechende Anzahl Füllzeichen in den Stream ein:

ostream& padN( ostream& os, int n)
{
    while (os && n--)
        os << os.fill();
    return os;
}

Nun muss noch der Manipulator definiert werden. Er stellt sich als einfache Funktion dar, die - eingefügt in einen Stream - über den Output Stream Operator von iomani die Vermittlung zwischen dem Stream und der eigentlichen Ausgabefunktion padn vornimmt:

omani pad( int n)
{
    return omani( &padN, n);
}

Lohn der Mühe

Nun lässt sich einfach schreiben:

cout << setfill('-') << "1" << pad(20) << "2" << endl;

was eine Ausgabe von 20 Füllzeichen zwischen "1" und "2" bewirkt. Hier zeigt sich eine Grundtendenz effektiver Softwareentwicklung (nicht nur in C++): Komplizierte Vorarbeit resultiert in einfacher Anwendbarkeit! Das Entscheidende ist, dass die zeitaufwendige Vorarbeit nur einmal getan werden muss - vom Resultat können viele Nutzer wiederholt profitieren.