wake-up-neo.net

Wie schreibe ich einen benutzerdefinierten Eingabestrom in C++?

Ich lerne gerade C++ (von Java kommend) und versuche zu verstehen, wie IO - Streams in C++ richtig verwendet werden. 

Nehmen wir an, ich habe eine Image-Klasse, die die Pixel eines Bildes enthält, und ich habe den Extraktionsoperator überladen, um das Bild aus einem Stream zu lesen:

istream& operator>>(istream& stream, Image& image)
{
    // Read the image data from the stream into the image
    return stream;
}

Jetzt kann ich so ein Bild lesen:

Image image;
ifstream file("somepic.img");
file >> image;

Jetzt möchte ich jedoch denselben Extraktionsoperator verwenden, um die Bilddaten aus einem benutzerdefinierten Stream zu lesen. Nehmen wir an, ich habe eine Datei, die das Bild in komprimierter Form enthält. Anstatt ifstream zu verwenden, möchte ich vielleicht meinen eigenen Eingabestrom implementieren. Zumindest würde ich das in Java machen. In Java würde ich eine benutzerdefinierte Klasse schreiben, die die InputStream-Klasse erweitert und die int read()-Methode implementiert. Das ist also ziemlich einfach. Und die Nutzung würde so aussehen:

InputStream stream = new CompressedInputStream(new FileInputStream("somepic.imgz"));
image.read(stream);

Wenn Sie also dasselbe Muster verwenden, möchte ich dies vielleicht in C++ tun:

Image image;
ifstream file("somepic.imgz");
compressed_stream stream(file);
stream >> image;

Aber vielleicht ist das der falsche Weg, weiß nicht. Das Erweitern der istream-Klasse sieht ziemlich kompliziert aus und nach einigem Suchen fand ich stattdessen ein paar Hinweise zum Erweitern von streambuf. Aber dieses Beispiel sieht für eine so einfache Aufgabe äußerst kompliziert aus. 

Was ist der beste Weg, um benutzerdefinierte Eingabe-/Ausgabeströme (oder Streambufs?) In C++ zu implementieren?

Lösung

Einige Leute schlugen vor, Iostreams überhaupt nicht zu verwenden und stattdessen Iteratoren, Boost oder eine benutzerdefinierte IO - Schnittstelle zu verwenden. Dies können gültige Alternativen sein, aber meine Frage betraf Iostreams. Die akzeptierte Antwort ergab den folgenden Beispielcode. Um das Lesen zu erleichtern, gibt es keine Header-/Code-Trennung und der gesamte std-Namespace wird importiert (ich weiß, dass dies in echtem Code eine schlechte Sache ist).

In diesem Beispiel werden vertikal-xor-kodierte Bilder gelesen und geschrieben. Das Format ist ziemlich einfach. Jedes Byte repräsentiert zwei Pixel (4 Bit pro Pixel). Jede Zeile ist mit der vorherigen Zeile xor'd. Diese Art der Kodierung bereitet das Bild für die Komprimierung vor (normalerweise sind viele 0-Byte-Werte vorhanden, die leichter zu komprimieren sind).

#include <cstring>
#include <fstream>

using namespace std;

/*** vxor_streambuf class ******************************************/

class vxor_streambuf: public streambuf
{
public:
    vxor_streambuf(streambuf *buffer, const int width) :
        buffer(buffer),
        size(width / 2)
    {
        previous_line = new char[size];
        memset(previous_line, 0, size);
        current_line = new char[size];
        setg(0, 0, 0);
        setp(current_line, current_line + size);
    }

    virtual ~vxor_streambuf()
    {
        sync();
        delete[] previous_line;
        delete[] current_line;
    }

    virtual streambuf::int_type underflow()
    {
        // Read line from original buffer
        streamsize read = buffer->sgetn(current_line, size);
        if (!read) return traits_type::eof();

        // Do vertical XOR decoding
        for (int i = 0; i < size; i += 1)
        {
            current_line[i] ^= previous_line[i];
            previous_line[i] = current_line[i];
        }

        setg(current_line, current_line, current_line + read);
        return traits_type::to_int_type(*gptr());
    }

    virtual streambuf::int_type overflow(streambuf::int_type value)
    {
        int write = pptr() - pbase();
        if (write)
        {
            // Do vertical XOR encoding
            for (int i = 0; i < size; i += 1)
            {
                char tmp = current_line[i];
                current_line[i] ^= previous_line[i];
                previous_line[i] = tmp;
            }

            // Write line to original buffer
            streamsize written = buffer->sputn(current_line, write);
            if (written != write) return traits_type::eof();
        }

        setp(current_line, current_line + size);
        if (!traits_type::eq_int_type(value, traits_type::eof())) sputc(value);
        return traits_type::not_eof(value);
    };

    virtual int sync()
    {
        streambuf::int_type result = this->overflow(traits_type::eof());
        buffer->pubsync();
        return traits_type::eq_int_type(result, traits_type::eof()) ? -1 : 0;
    }

private:
    streambuf *buffer;
    int size;
    char *previous_line;
    char *current_line;
};


/*** vxor_istream class ********************************************/

class vxor_istream: public istream
{
public:
    vxor_istream(istream &stream, const int width) :
        istream(new vxor_streambuf(stream.rdbuf(), width)) {}

    virtual ~vxor_istream()
    {
        delete rdbuf();
    }
};


/*** vxor_ostream class ********************************************/

class vxor_ostream: public ostream
{
public:
    vxor_ostream(ostream &stream, const int width) :
        ostream(new vxor_streambuf(stream.rdbuf(), width)) {}

    virtual ~vxor_ostream()
    {
        delete rdbuf();
    }
};


/*** Test main method **********************************************/

int main()
{
    // Read data
    ifstream infile("test.img");
    vxor_istream in(infile, 288);
    char data[144 * 128];
    in.read(data, 144 * 128);
    infile.close();

    // Write data
    ofstream outfile("test2.img");
    vxor_ostream out(outfile, 288);
    out.write(data, 144 * 128);
    out.flush();
    outfile.close();

    return 0;
}
43
kayahr

Der richtige Weg zum Erstellen eines neuen Streams in C++ besteht darin, von std::streambuf abzuleiten und die underflow()-Operation zum Lesen und die overflow()- und sync()-Operation zum Schreiben zu überschreiben. Zu Ihrem Zweck erstellen Sie einen Filterstrompuffer, der einen weiteren Streampuffer (und möglicherweise einen Stream, aus dem der Streampuffer mithilfe von rdbuf() extrahiert werden kann) als Argument verwendet und seine eigenen Operationen in Bezug auf diesen Streampuffer implementiert.

Die grundlegende Gliederung eines Stream-Puffers sieht etwa so aus:

class compressbuf
    : public std::streambuf {
    std::streambuf* sbuf_;
    char*           buffer_;
    // context for the compression
public:
    compressbuf(std::streambuf* sbuf)
        : sbuf_(sbuf), buffer_(new char[1024]) {
        // initialize compression context
    }
    ~compressbuf() { delete[] this->buffer_; }
    int underflow() {
        if (this->gptr() == this->egptr()) {
            // decompress data into buffer_, obtaining its own input from
            // this->sbuf_; if necessary resize buffer
            // the next statement assumes "size" characters were produced (if
            // no more characters are available, size == 0.
            this->setg(this->buffer_, this->buffer_, this->buffer_ + size);
        }
        return this->gptr() == this->egptr()
             ? std::char_traits<char>::eof()
             : std::char_traits<char>::to_int_type(*this->gptr());
    }
};

Wie underflow() genau aussieht, hängt von der verwendeten Komprimierungsbibliothek ab. Die meisten Bibliotheken, die ich verwendet habe, verfügen über einen internen Puffer, der gefüllt werden muss und der die noch nicht verbrauchten Bytes enthält. Normalerweise ist es relativ einfach, die Dekomprimierung in underflow() einzuhaken.

Nachdem der Stream-Puffer erstellt wurde, können Sie ein std::istream-Objekt einfach mit dem Stream-Puffer initialisieren:

std::ifstream fin("some.file");
compressbuf   sbuf(fin.rdbuf());
std::istream  in(&sbuf);

Wenn Sie den Stream-Puffer häufig verwenden, können Sie die Objektkonstruktion in eine Klasse einbetten, z. B. icompressstream. Dies ist etwas schwierig, da die Basisklasse std::ios eine virtuelle Basis ist und der tatsächliche Speicherort des Stream-Puffers ist. Um den Stream-Puffer vor dem Übergeben eines Zeigers auf einen std::ios zu erstellen, müssen Sie einige Blöcke durchgehen: Es ist die Verwendung einer virtual-Basisklasse erforderlich. So könnte das ungefähr aussehen:

struct compressstream_base {
    compressbuf sbuf_;
    compressstream_base(std::streambuf* sbuf): sbuf_(sbuf) {}
};
class icompressstream
    : virtual compressstream_base
    , public std::istream {
public:
    icompressstream(std::streambuf* sbuf)
        : compressstream_base(sbuf)
        , std::ios(&this->sbuf_)
        , std::istream(&this->sbuf_) {
    }
};

(Ich habe gerade diesen Code ohne eine einfache Möglichkeit eingegeben, um zu testen, ob er einigermaßen korrekt ist. Bitte erwarten Sie Tippfehler, aber der Gesamtansatz sollte wie beschrieben funktionieren.)

52
Dietmar Kühl

boost (was Sie bereits haben sollten, wenn Sie ernsthaft mit C++ arbeiten), hat eine ganze Bibliothek zum Erweitern und Anpassen von IO Streams: boost.iostreams

Insbesondere hat es bereits die Dekomprimierung von Streams für einige gängige Formate ( bzip2 , gzlib und zlib ).

Wie Sie gesehen haben, ist das Erweitern von streambuf möglicherweise ein schwieriger Job, aber die Bibliothek macht es recht einfach, Ihre eigenen Filter-Streambufs zu schreiben falls Sie eine benötigen.

8
Cubbi

Tun Sie es nicht, es sei denn, Sie möchten einen schrecklichen Tod mit abscheulichen Design sterben. IOstreams sind die schlechteste Komponente der Standardbibliothek - sogar noch schlimmer als Locales. Das Iterator-Modell ist viel nützlicher und Sie können mit istream_iterator vom Stream in den Iterator konvertieren.

4
Puppy

Ich stimme @DeadMG zu und würde die Verwendung von Iostreams nicht empfehlen. Abgesehen von schlechtem Design ist die Leistung oft schlechter als bei einfachen alten C-Anschlüssen. Ich würde mich jedoch nicht an eine bestimmte E/A-Bibliothek halten. Stattdessen würde ich eine Schnittstelle (abstrakte Klasse) erstellen, die alle erforderlichen Vorgänge enthält.

class Input {
 public:
  virtual void read(char *buffer, size_t size) = 0;
  // ...
};

Dann können Sie diese Schnittstelle für C I/O, Iostreams, mmap oder was auch immer implementieren.

0
vitaut

Möglicherweise ist dies möglich, aber ich habe das Gefühl, dass es nicht die "richtige" Verwendung dieser Funktion in C++ ist. Die Operatoren iostream >> und << sind für relativ einfache Operationen gedacht, z. B. zum Schreiben von "Name, Straße, Ort, Postleitzahl" eines class Person, nicht zum Analysieren und Laden von Bildern. Das geht viel besser mit stream :: read () - mit Image(astream);, und Sie können einen Stream für die Komprimierung implementieren, wie von Dietmar beschrieben. 

0
Mats Petersson