std::streambuf

Manchmal (zugegebenermaßen selten) muss man ein eigenes streambuf-Objekt implementieren. Das geht (in etwa) so:

Video

Quelltext

#include <iostream>
#include <streambuf>
#include <fstream>
#include <exception>
#include <algorithm>

using namespace std;

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

public:
    
    ROT13Buffer(streambuf *buf)
        : m_buf { buf }
    {
        setp(m_buffer, m_buffer+sizeof(m_buffer)-1);  // -1 vereinfacht overflow erheblich
    }
    
    streambuf *buffer() const
    {
        return m_buf;
    }
    
private:
    
    void do_output()
    {
        transform(pbase(), pptr(), pbase(), 
                            [](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;
                                }});
        m_buf->sputn(pbase(), pptr()-pbase());
        setp(m_buffer, m_buffer+sizeof(m_buffer)-1);
    }
    
    int_type overflow(int_type ch)
    {
        if (ch != traits_type::eof()) {
            *pptr() = ch;
            pbump(1);
            do_output();
        }
        return ch;
    }
    
    int sync()
    {
        do_output();
        return 0;
    }
};

int main()
{
    ROT13Buffer r_buf { cout.rdbuf() }; 
    ostream os { &r_buf };
   
    os << cin.rdbuf();
    os.flush();
}

Erklärung

Der Streambuffer hier im Beispiel "verschlüsselt" alle Eingaben mit ROT13 und gibt sie auf einen eingebetteten anderen streambuf aus. Grundlegend ist ein Streambuffer ein Pufferbereich, in dem char oder wchar_t (oder ähnliches) gesammelt werden können, bevor sie ausgegeben werden. Typischerweise dient das der Steigerung der Geschwindigkeit (100 Zeichen einzeln an die Konsole reichen ist schlicht deutlich langsamer, als alle 100 Zeichen auf einmal). Die Basisklasse std::streambuf bietet dazu eine öffentliche Schnittstelle, über die Zeichen aus dem Pufferbereich gelesen oder in diesen geschrieben werden können. Sie selbst bietet allerdings gar keinen internen Puffer an, sondern soll abgeleitet werden, um das tatsächliche Pufferkonzept zu implementieren. Zuallererst muss ihr dazu mitgeteilt werden, welchen Speicherbereich sie als Puffer verwenden kann. Das geschieht über die Methode setp() (Zeile 19 direkt im Konstruktor). Diese Methode nimmt 2 Parameter: den Pointer auf das erste Element des Schreibpuffers (deswegen setp: das p steht für "put") und den Pointer hinter das letzte Element. Intern verwaltet std::streambuf dann drei Pointer, die über entsprechende benannte Methoden abgefragt werden können:

  • pbase -  Der Anfang des Puffers. Dieser bleibt fest auf dem Wert, der beim letzten Aufruf von setp() als erster Parameter übergeben wurde.
  • epptr - Das Ende des Puffers. Dieser Pointer zeigt auf das erste Element hinter dem Puffer (gleiches Konzept, wie bei den Iteratoren der Containers Library). Dieser Wert bleibt fest auf dem zweiten Parameter des letzten Aufrufs von setp()
  • pptr - Die aktuelle Schreibposition. Dieser Pointer wird mit jedem geschriebenen Zeichen verschoben.

Anhand dieser Pointer kann die Pufferverwaltung in std::streambuf nun feststellen, wenn der Puffer voll ist. Nach dem Aufruf von setp() gilt erstmal pbase == pptr. Werden nun Zeichen im Puffer abgelegt, so wandert pptr immer weiter in Richtung epptr (streambuf setzt voraus, dass gilt: epptr >= pbase. Der Puffer darf also nicht "verkehrtrum" im Speicher liegen). Wenn gilt pptr == epptr und ein weiteres Zeichen geschrieben werden soll, dann erkennt das Puffermanagement einen Überlauf und ruft die interne Funktion overflow auf. Diese erhält das überlaufende Zeichen (es konnte ja nicht mehr in den Puffer geschrieben werden) und zeigt an, dass der Puffer bitte leerzuräumen ist. Hier setzt die Implementierung eines eigenen streambuf an: die Methode overflow muss die zwischengespeicherten Daten wegschreiben und den Puffer danach zurücksetzen. Allerdings ist dabei noch das übergebene Zeichen zu beachten. Verwirft man das einfach, so fehlt es nämlich in der Ausgabe. Hier im Beispiel wird dieser Sonderfall behandelt, indem der an std::streambuf übergebene Puffer im ein Element kleiner ist, als der tatsächlich vorhandene (m_buffer). Dadurch ist immer noch das letzte Element in diesem frei, wenn overflow() aufgerufen wird. Ergo kann das Zeichen einfach an die letzte Stelle des Puffers geschrieben werden und dieser dann ausgegeben.

Im Beispiel wird der Puffer ROT13-"verschlüsselt" und dann auf einem zugrundeliegenden streambuf (m_buf) ausgegeben. Dazu wird die Funktion transform aus <algorithm> verwenden. Diese erwartet einen Start- und Enditerator eines Quellbereiches (Zeiger funktionieren hervorragend als Iteratoren, können also hier problemlos zum Einsatz kommen), einen Startiterator auf den Ausgabebereich und eine Funktionsobjekt, welches auf jedes einzelne Element angewandt werden soll. Hier im Beispiel dienen pbase und pptr als Anfangs- und Enditeratoren (und decken damit den aktuell gefüllten Bereich des internen Puffers ab). pbase wird ebenso als Startiterator für die Ausgabe verwendet, der Puffer wird so in-place überschrieben. Das Funktionsobjekt rotiert alle Buchstaben im Alphabet um 13 Stellen und lässt alle anderen Zeichen in Ruhe. Danach wird mittels sputn() der Puffer noch auf dem zugrundeliegenden streambuf ausgegeben (Zeile 41) und der Puffer zurückgesetzt (indem Anfangs- und Endpointer wieder auf die Ursprungswerte gestellt werden. Effektiv setzt das eigentlich nur pptr zurück, da die beiden Arraygrenzen ja unverändert bleiben). 

Unter Umständen kann es dazu kommen, dass ein Pufferinhalt ausgegeben werden muss, bevor der Puffer voll ist. Das ist beispielsweise der Fall, wenn auf dem zugehörigen ostream ein flush() aufgerufen wird. Dafür ist die Methode sync() da. Diese tut nichts anderes, als den vorliegenden Pufferinhalt sofort auszugeben. 

Hier im Beispiel wird der ROT13Buffer verwendet, indem er den Ausgabepuffer von cout einpackt. Alles, was in unseren ROT13Buffer geschrieben wird, landet also auf der Konsole und wird vorher eben ROT13-verschlüsselt. Um das zu testen packen wir das ROT13Buffer-Objekt in einen ostream ein und geben auf diesem den Lesepuffer von cin aus. Das führt dazu, dass der Lesepuffer bis zum EOF gelesen wird und alle Daten auf dem ostream ausgegeben werden. So, wie das hier im Beispiel verschalten ist, werden also Inhalte von cin gelesen und ROT13-verschlüsselt auf cout wieder ausgeben.

Anmerkung: Relativ viel von meinem Verständnis der streambuf-Objekte stammt aus einem sehr guten Artikel im Netz. Wer Englisch kann, kann gern auf http://www.mr-edd.co.uk/blog/beginners_guide_streambuf weiterlesen.