std::streambuf (2)

std::streambuf dient (bei richtiger Implementierung) auch zu Eingabe von Daten. Im Prinzip muss man nur die richtige Schnittstelle bedienen.

Video

Quelltext

#include <streambuf>
#include <iostream>
#include <algorithm>

using namespace std;

class ROT13IBuffer : public std::streambuf
{
    streambuf *m_buf;
    char m_buffer[100];

public:

    ROT13IBuffer(std::streambuf *buf)
        : m_buf { buf }
    {
        setg(m_buffer, m_buffer, m_buffer);
    }
    
    streambuf *buffer() const
    {
        return m_buf;
    }
    
private:
    
    int underflow() override
    {
        auto chars = m_buf->sgetn(m_buffer, sizeof(m_buffer));
        if (chars == 0) {
            return traits_type::eof();
        }
        transform(m_buffer, m_buffer+chars, m_buffer,
                            [](char elem) -> char {
                                if (elem >= 'a' && elem <= 'z')
                                    return elem + 13 <= 'z' ? elem + 13 : elem + 13 - 26;
                                else if (elem >= 'A' && elem <= 'Z') {
                                    return elem + 13 <= 'Z' ? elem + 13 : elem + 13 - 26;
                                }
                                else {
                                    return elem;
                                }});
        setg(m_buffer, m_buffer, m_buffer+chars);
        return traits_type::to_int_type(m_buffer[0]);
    }
    
    int pbackfail(int c) override
    {
        cerr << "pbackfail: " << static_cast<char>(c) << endl;
        if (gptr() - 1 < m_buffer) {
            // kein Platz im Puffer
            return traits_type::eof();
        }
        setg(eback(), gptr()-1, egptr());
        if (c != traits_type::eof()) {
            *gptr() = traits_type::to_char_type(c);
        }
        return traits_type::to_int_type(*gptr());
    }
};

int main()
{
    ROT13IBuffer r_buf { cin.rdbuf() };
    istream is { &r_buf };
    std::string line;
    
    getline(is, line);
    
    cout << "Input ergibt dekodiert: '" << line << "'\n";
    
    char c;
    is.get(c);
    cout << "Nächster char: " << c << endl;
    is.putback(c);
    is.get(c);
    cout << "Nach putback 1: " << c << endl;    
    is.putback('X');
    is.get(c);
    cout << "Nach putback 2: " << c << endl;    
}

Erklärung

Die Schnittstelle für das Einlesen von Daten aus einem streambuf ist im Prinzip baugleich zur Ausgabeschnittstelle: intern wird ein Puffer verwaltet, in den drei Zeiger zeigen: ein Anfangszeiger, ein Lesezeiger für die aktuell zu lesende Position und ein Endzeiger, der die Begrenzung des Puffers anzeigt. Diese werden über die Methode setg() verwaltet. Hier im Beispiel werden anfangs im Konstruktor alle drei Pointer auf den Pufferanfang gesetzt. Dadurch wird angezeigt, dass der Puffer noch leer ist. Alternativ könnte man im Konstruktor natürlich auch den ersten Block in den Puffer einlesen. Die Implementierung von std::streambuf liest nun solange Zeichen aus dem Puffer, bis der Lesezeiger am Ende angelangt ist. Ist das der Fall, wird die Methode underflow() aufgerufen, deren Aufgabe es ist, neue Daten aus der Datenquelle zu lesen. 

Im Beispiel oben liest die Methode zuerst den Puffer aus dem zugrundeliegenden streambuf (Zeile 29). Können hierbei keine Daten gelesen werden, dann liefert die Methode EOF zurück um anzuzeigen, dass die Datenquelle am Ende angelangt ist. Wurden Daten eingelesen, so werden sie analog zur Ausgabe beim letzten Mal mittels std::transform() und einer passenden Lambda-Funktion ROT13-entschlüsselt. setg() setzt dann die Pufferzeiger passend um anzuzeigen, dass nun Daten vorhanden sind (Anfangs- und Lesezeiger zeigen beide auf den Pufferanfang, der Endzeiger zeigt hinter das letzte gelesene Zeichen). Laut Spezifikation liefert underflow() das erste neu gelesene Zeichen zurück (Zeile 44). Dieses wird hierbei mittels traits_type::to_int_type() in einen Integer umgewandelt. Im Allgemeinen wird das einfach nur den Wert des Characters 1:1 in einen Integer umgewandelt. Je nach gesetzten traits kann das allerdings durchaus auch komplexer ausgeführt werden.

Eine zweite Funktionalität, die std::streambuf anbietet (und die auch von den umgebenden std::istream an den Nutzer geführt wird), ist die Rückgabe von Zeichen an den Eingabestrom. Zweck ist hier, ein Zeichen aus dem Eingabestrom zu lesen um bspw. irgendwelche Logik zu verzweigen (bspw. einen Tag, der die kommenden Daten im Strom identifiziert). In diesem Fall möchte man oft das Zeichen nur kurz anschauen und dann gleich wieder zurücklegen, um es im eigentlichen Lesecode wieder mit lesen zu können. Das Zeichen wird hierbei mittels std::istream::putback() zurückgegeben. Diese Methode übernimmt das zurückzugebende Zeichen als Parameter. Im Normalfall wird dieses Zeichen einfach an der Stelle vor dem aktuellen Lesepointer in den Eingabepuffer abgelegt. Hierbei können nun zwei Fehlerfälle auftreten: einersetzt kann direkt am Anfang des Lesepuffers ein Zeichen zurückgelegt werden (was dann ja vor den eigentlichen Puffer gespeichert werden müsste) und andererseits kann der Nutzer ein anderes Zeichen, als das zuletzt gelesene, zurücklegen wollen. Beide Fälle betrachtet std::streambuf als Fehler und bietet eine virtuelle Funktion namens pbackfail() an, um diesen zu behandeln. In der Standardimplementierung liefert diese Funktion immer EOF, womit angezeigt wird, dass ein Zurücklegen nicht möglich ist. Hier im Beispiel soll zumindest in einem der beiden Fälle (der Nutzer versucht ein anderes Zeichen zurückzulegen) das Schreiben erlaubt sein. Dazu wird die Methode pbackfail() überschrieben. Ist der Lesepointer am Anfang des Puffers, so ist das weiter ein Fehlerfall, der mit EOF angezeigt wird (Zeilen 50-53). Andernfalls gibt es noch einen Sonderfall zu beachten: ist das übergebene Zeichen EOF, dann soll das im internen Puffer vorliegende Zeichen unverändert bleiben (faktisch also nur der Lesepointer um eins verringert werden (if-Bedingung in Zeile 55). pbackfail() liefert am Ende das Zeichen zurück, was als nächstes im Puffer gelesen werden würde.