Comment écrire un stream d’entrée personnalisé en C ++

J’apprends actuellement C ++ (venant de Java) et j’essaie de comprendre comment utiliser correctement les stream IO en C ++.

Disons que j’ai une classe Image qui contient les pixels d’une image et j’ai surchargé l’opérateur d’extraction pour lire l’image d’un stream:

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

Alors maintenant, je suis capable de lire une image comme celle-ci:

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

Mais maintenant, je veux utiliser le même opérateur d’extraction pour lire les données d’image d’un stream personnalisé. Disons que j’ai un fichier qui contient l’image sous forme compressée. Donc, au lieu d’utiliser ifstream, je pourrais vouloir implémenter mon propre stream d’entrée. Au moins, c’est comme ça que je le ferais en Java. En Java, j’écrirais une classe personnalisée étendant la classe InputStream et implémentant la méthode int read() . Donc c’est assez facile. Et l’utilisation ressemblerait à ceci:

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

Donc, en utilisant le même modèle, je veux peut-être le faire en C ++:

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

Mais peut-être que ce n’est pas la bonne façon, je ne sais pas. L’extension de la classe istream est plutôt compliquée et après quelques recherches, j’ai trouvé quelques conseils sur l’extension de streambuf . Mais cet exemple semble terriblement compliqué pour une tâche aussi simple.

Alors, quel est le meilleur moyen d’implémenter des stream d’entrée / sortie personnalisés (ou des streambufs?) En C ++?

Solution

Certaines personnes ont suggéré de ne pas utiliser d’iostreams et d’utiliser plutôt des iterators, des boost ou une interface IO personnalisée. Celles-ci peuvent être des alternatives valables, mais ma question concernait les iostreams. La réponse acceptée a abouti à l’exemple de code ci-dessous. Pour faciliter la lecture, il n’y a pas de séparation en-tête / code et tout l’espace de noms std est importé (je sais que c’est une mauvaise chose en code réel).

Cet exemple concerne la lecture et l’écriture d’images à codage vertical ou xor. Le format est assez facile. Chaque octet représente deux pixels (4 bits par pixel). Chaque ligne est xor avec la ligne précédente. Ce type de codage prépare l’image à la compression (il en résulte généralement beaucoup de 0 octets plus faciles à compresser).

 #include  #include  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 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; } 

La méthode appropriée pour créer un nouveau stream en C ++ consiste à dériver de std::streambuf et à remplacer l’opération std::streambuf underflow() pour la lecture et les opérations overflow() et sync() pour l’écriture. Pour ce faire, vous créeriez un tampon de stream de filtrage qui prend un autre tampon de stream (et éventuellement un stream à partir duquel le tampon de stream peut être extrait en utilisant rdbuf() ) et implémente ses propres opérations en termes de tampon de stream.

Le contour de base d’un tampon de stream serait quelque chose comme ceci:

 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::eof() : std::char_traits::to_int_type(*this->gptr()); } }; 

L’aspect de underflow() dépend exactement de la bibliothèque de compression utilisée. La plupart des bibliothèques que j’ai utilisées conservent un tampon interne qui doit être rempli et qui conserve les octets qui ne sont pas encore consommés. En règle générale, il est assez facile d’accrocher la décompression à un underflow() .

Une fois le tampon créé, vous pouvez simplement initialiser un object std::istream avec le tampon de stream:

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

Si vous utilisez fréquemment le tampon de stream, vous pouvez encapsuler la construction de l’object dans une classe, par exemple icompressstream . Cela est un peu compliqué car la classe de base std::ios est une base virtuelle et correspond à l’emplacement réel où le tampon de stream est stocké. Construire le tampon de stream avant de passer un pointeur à un std::ios nécessite donc de sauter quelques obstacles: il nécessite l’utilisation d’une classe de base virtual . Voici comment cela pourrait ressembler:

 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_) { } }; 

(Je viens de taper ce code sans moyen simple de vérifier qu’il est raisonnablement correct; veuillez vous attendre à des fautes de frappe mais l’approche globale devrait fonctionner comme décrit)

boost (que vous devriez déjà avoir si vous êtes sérieux à propos de C ++), possède une bibliothèque entière dédiée à l’extension et à la personnalisation des stream IO: boost.iostreams

En particulier, il a déjà des stream de décompression pour quelques formats populaires ( bzip2 , gzlib et zlib )

Comme vous l’avez vu, l’extension de streambuf peut être un travail complexe, mais la bibliothèque facilite l’ écriture de votre propre streambuf de filtrage si vous en avez besoin.

Ne le faites pas, à moins que vous ne vouliez mourir d’une terrible mort de conception hideuse. Les stream IO sont le pire composant de la bibliothèque standard – pire que les environnements locaux. Le modèle d’iterator est beaucoup plus utile et vous pouvez convertir du stream en iterator avec istream_iterator.

Il est probablement possible de le faire, mais je pense que ce n’est pas l’utilisation “correcte” de cette fonctionnalité en C ++. Les opérateurs iostream >> et << sont destinés à des opérations assez simples, telles que l'écriture du "nom, rue, ville, code postal" d'une class Person , et non pour l’parsing et le chargement d’images. C’est beaucoup mieux fait en utilisant le stream :: read () – en utilisant Image(astream); , et vous pouvez implémenter un stream pour la compression, comme décrit par Dietmar.

Je suis d’accord avec @DeadMG et je ne recommande pas d’utiliser iostreams. Hormis une conception médiocre, les performances sont souvent inférieures à celles des anciennes E / S de style C. Je ne m’en tiendrai pas à une bibliothèque d’E / S particulière, mais je créerais plutôt une interface (classe abstraite) ayant toutes les opérations requirejses, par exemple:

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

Ensuite, vous pouvez implémenter cette interface pour CI / O, iostreams, mmap ou autres.