web-dev-qa-db-fra.com

Comment écrire un flux d'entrée personnalisé en C++

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

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

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

Alors maintenant, je peux 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 à partir d'un flux 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 flux d’entrée. Au moins c'est comme ça que je le ferais en Java. En Java, je voudrais écrire une classe personnalisée qui étend la classe InputStream et implémente 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 motif, je veux peut-être faire cela en C++:

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

Mais peut-être que c'est la mauvaise façon, je ne sais pas. L'extension de la classe istream semble assez compliquée et, après quelques recherches, j'ai trouvé quelques astuces pour étendre streambuf à la place. Mais cet exemple semble terriblement compliqué pour une tâche aussi simple. 

Alors, quel est le meilleur moyen d'implémenter des flux d'entrées/sorties personnalisés (ou streambufs?) En C++?

Solution

Certaines personnes ont suggéré de ne pas utiliser du tout iostreams et d'utiliser plutôt des itérateurs, un boost ou une interface personnalisée IO. Ce sont peut-être des alternatives valables, mais ma question portait sur 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 l'ensemble de l'espace de noms std est importé (je sais que c'est une mauvaise chose dans le code réel).

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

#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

La manière appropriée de créer un nouveau flux en C++ consiste à dériver de std::streambuf et à remplacer l'opération underflow() en lecture et les opérations overflow() et sync() en écriture. Pour ce faire, vous devez créer un tampon de flux de filtrage qui prend un autre tampon de flux (et éventuellement un flux à partir duquel le tampon de flux peut être extrait à l'aide de rdbuf()) en tant qu'argument et implémente ses propres opérations en termes de tampon de flux.

Le contour de base d'un tampon de flux ressemblerait à 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<char>::eof()
             : std::char_traits<char>::to_int_type(*this->gptr());
    }
};

L’apparence 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 utilisés. En règle générale, il est assez facile d’accrocher la décompression à underflow().

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

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

Si vous comptez utiliser fréquemment le tampon de flux, vous souhaiterez peut-être encapsuler la construction de l'objet dans une classe, par exemple icompressstream. Cela est un peu délicat, car la classe de base std::ios est une base virtuelle et représente l'emplacement réel où le tampon de flux est stocké. Construire le tampon de flux avant de passer un pointeur sur un std::ios nécessite donc de sauter à travers quelques obstacles: Il nécessite l’utilisation d’une classe de base virtual. Voici à quoi 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; attendez-vous à des fautes de frappe, mais l'approche globale devrait fonctionner comme décrit)

52
Dietmar Kühl

Ne le faites pas, sauf si vous voulez mourir d'une mort terrible de conception hideuse. Les IOstream sont le pire composant de la bibliothèque Standard - encore pire que les locales. Le modèle d'itérateur est beaucoup plus utile, et vous pouvez convertir un flux en itérateur avec istream_iterator.

4
Puppy

Je suis d'accord avec @DeadMG et ne recommanderais pas d'utiliser iostreams. Outre une conception médiocre, les performances sont souvent pires que celles des anciennes E/S de type C. Je ne voudrais pas m'en tenir à une bibliothèque d'E/S particulière, mais je créerais plutôt une interface (classe abstraite) comportant toutes les opérations requises, par exemple:

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

Ensuite, vous pouvez implémenter cette interface pour C I/O, iostreams, mmap ou autre.

0
vitaut

C'est probablement possible, mais j'estime que ce n'est pas la "bonne" utilisation de cette fonctionnalité en C++. Les opérateurs >> et << sont destinés à des opérations assez simples, telles que l’écriture du "nom, rue, ville, code postal" d’un class Person, et non à l’analyse et au chargement des images. C'est beaucoup mieux fait en utilisant stream :: read () - en utilisant Image(astream);, et vous pouvez implémenter un flux pour la compression, comme décrit par Dietmar. 

0
Mats Petersson