Подтвердить что ты не робот

Как написать настраиваемый поток ввода в С++

В настоящее время я изучаю С++ (Coming from Java), и я пытаюсь понять, как правильно использовать потоки ввода-вывода на С++.

Скажем, у меня есть класс Image, который содержит пиксели изображения, и я перегрузил оператор извлечения, чтобы прочитать изображение из потока:

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

Итак, теперь я могу прочитать такое изображение:

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

Но теперь я хочу использовать один и тот же оператор извлечения для чтения данных изображения из пользовательского потока. Скажем, у меня есть файл, который содержит изображение в сжатой форме. Поэтому вместо использования ifstream я могу реализовать собственный поток ввода. По крайней мере, так, как я буду делать это на Java. В Java я бы написал пользовательский класс, расширяющий класс InputStream и реализующий метод int read(). Так что это довольно легко. И использование будет выглядеть так:

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

Поэтому, используя тот же шаблон, возможно, я хочу сделать это в С++:

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

Но, возможно, это неправильный путь, не знаю. Расширение класса istream выглядит довольно сложно, и после некоторого поиска я нашел несколько советов о расширении streambuf. Но этот example выглядит ужасно сложным для такой простой задачи.

Итак, какой лучший способ реализовать пользовательские потоки ввода/вывода (или streambufs?) в С++?

Решение

Некоторые люди предложили вообще не использовать iostreams и вместо этого использовать итераторы, boost или пользовательский интерфейс ввода-вывода. Это могут быть действительные альтернативы, но мой вопрос касался iostreams. В принятом ответе приведен пример кода ниже. Для упрощения чтения отсутствует разделение заголовка/кода, и импортируется все пространство имен std (я знаю, что это плохое в реальном коде).

В этом примере речь идет о чтении и записи изображений с вертикальным xor-кодированием. Формат довольно прост. Каждый байт представляет два пикселя (4 бит на пиксель). Каждая строка имеет xor'd с предыдущей строкой. Этот вид кодирования подготавливает изображение для сжатия (обычно это приводит к большому количеству 0-байтов, которые легче сжимать).

#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;
}
4b9b3361

Ответ 1

Правильный способ создания нового потока в С++ состоит в выводе std::streambuf и переопределении операции underflow() для чтения и операций overflow() и sync() для записи. Для вашей цели вы создадите буфер потока фильтрования, который будет использовать другой буфер потока (и, возможно, поток, из которого буфер потока может быть извлечен с помощью rdbuf()) в качестве аргумента, и реализует свои собственные операции в терминах этого буфера потока.

Основной контур буфера потока будет примерно таким:

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());
    }
};

Как выглядит underflow() точно зависит от используемой библиотеки сжатия. Большинство библиотек, которые я использовал, сохраняют внутренний буфер, который необходимо заполнить и который сохраняет байты, которые еще не используются. Как правило, довольно легко подключить декомпрессию к underflow().

После создания буфера потока вы можете просто инициализировать объект std::istream с помощью буфера потока:

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

Если вы собираетесь часто использовать буфер потока, вы можете инкапсулировать конструкцию объекта в класс, например, icompressstream. Это немного сложно, потому что базовый класс std::ios является виртуальной базой и является фактическим местом, где хранится буфер потока. Для построения буфера потока перед передачей указателю на std::ios требуется переходить через несколько обручей: для этого требуется использование базового класса virtual. Вот как это выглядит примерно так:

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

(я просто набрал этот код без простого способа проверить, что он достаточно правильный, пожалуйста, ожидайте опечатки, но общий подход должен работать, как описано)

Ответ 2

boost (который должен быть уже, если вы серьезно относитесь к С++), имеет целую библиотеку, предназначенную для расширения и настройки потоков ввода-вывода: boost.iostreams

В частности, он уже распаковывает потоки для нескольких популярных форматов (bzip2, gzlib, и zlib)

Как вы видели, расширение streambuf может быть связанной задачей, но библиотека упрощает писать собственный поток streambuf фильтрации, если вам это нужно.

Ответ 3

Не надо, если ты не хочешь умереть от ужасной смерти отвратительного дизайна. IOstreams являются худшим компонентом стандартной библиотеки - даже хуже, чем локали. Модель итератора гораздо более полезна, и вы можете конвертировать из потока в итератор с помощью istream_iterator.

Ответ 4

Я согласен с @DeadMG и не рекомендую использовать iostreams. Помимо плохого дизайна производительность часто хуже, чем у обычного старого ввода-вывода C-стиля. Я бы не придерживался конкретной библиотеки ввода-вывода, но вместо этого я бы создал интерфейс (абстрактный класс), который имеет все необходимые операции, например:

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

Затем вы можете реализовать этот интерфейс для C I/O, iostreams, mmap или что-то еще.

Ответ 5

Возможно, это возможно, но я чувствую, что это не "правильное" использование этой функции на С++. Iostream → и < операторы предназначены для довольно простых операций, таких как writing "имя, улица, город, почтовый индекс" class Person, а не для разбора и загрузки изображений. Это гораздо лучше сделано с помощью stream:: read() - с помощью Image(astream);, и вы можете реализовать поток для сжатия, как описано Dietmar.

Ответ 6

Я думаю, что main(), приведенный в решении в вопросе, имеет небольшую, но важную ошибку. Ifstream и ofstream должны быть открыты в двоичном режиме:

int main()
{
    // Read data
    ifstream infile("test.img", ios::binary);
    ...

    // Write data
    ofstream outfile("test2.img", ios::binary);
    ...
}

Без этого я обнаружил, что чтение файла преждевременно закончилось в Windows

(Я бы добавил это как комментарий, но у меня еще нет 50 репутации)