Как создать поток, который обрабатывает как ввод, так и вывод в С++?

Я пытаюсь создать класс, который будет как входным, так и выходным потоком (например, std::cout и std::cin ). Я попытался перегрузить операторы << и >>, но потом понял, что писать такой код неразумно (поскольку это подход к переписыванию потоков C++), а поддерживать очень сложно, когда доступны такие классы, как std::basic_iostream, std::basic_ostream, std::basic_istream в стандартной библиотеке C++, потому что мне приходится перегружать операторы для каждого типа. Итак, я попытался определить свой класс следующим образом:

#include <istream>

class MyStream : public std::basic_iostream<char> {
public:
    MyStream() : std::basic_iostream<char>(stream_buffer) {}
};

Моя проблема связана с первым аргументом в конструкторе std::basic_iostream<char> . Начиная с cppreference, std::basic_iostream::basic_iostream принимает указатель на буфер потока, полученный из std::basic_streambuf :

explicit basic_iostream( std::basic_streambuf<CharT,Traits>* sb );

Я читал и пробовал примеры из главы 38 Руководства пользователя стандартной библиотеки Apache C++. В нем говорится, что я должен передать указатель на буфер потока, и есть три способа сделать это:

  • Создать буфер потока перед инициализацией класса
  • Возьмите буфер потока из другого потока (используя rdbuf() или аналогичный элемент)
  • Определите объект basic_streambuf как защищенный или закрытый элемент

Последний вариант лучше всего подходит для моей цели, но если я напрямую создам объект из класса std::basic_streambuf, он ничего не сделает, не так ли? Поэтому я определил еще один класс, производный от std::basic_streambuf<char>. Но на этот раз я не могу понять, какие функции определить, потому что я не знаю, какая функция вызывается при вставке, извлечении и сбросе данных.

Как создать поток с пользовательскими функциями?


Обратите внимание, что это попытка создать стандартное руководство по созданию потоков C++ и буферов потоков.


person Akib Azmain    schedule 22.07.2020    source источник
comment
У вас есть правильная идея. basic_streambuf ничего не делает, поэтому вам нужно получить класс от basic_streambuf, который делает то, что вам нужно. Но это слишком большая тема, чтобы ответить здесь. На эту тему есть хорошая книга. Или вы можете рискнуть и поискать в Google.   -  person john    schedule 22.07.2020
comment
Вот пример использования ЖК-дисплея в качестве std::ostream: github.com/amanuellperez/mcu/blob/master/src/dev/. Лучшее руководство по реализации iostream — это стандарт (проблема в том, что стандарт — это не простая лекция).   -  person Antonio    schedule 22.07.2020
comment
Более сложный пример — использовать UART как std::iostream: github.com/amanuellperez/mcu/blob/master/src/avr/ . Извините, часть комментариев на испанском, но все ссылки на стандарт на английском.   -  person Antonio    schedule 22.07.2020
comment
@john Я не думаю, что вопрос о том, как реализовать streambuf, слишком велик, чтобы отвечать здесь. Он слишком велик для меня, чтобы ответить здесь прямо сейчас, но может поместиться в формате ответа.   -  person user253751    schedule 22.07.2020
comment
@ user253751 Я ответил на свой вопрос. Все в порядке?   -  person Akib Azmain    schedule 25.09.2020
comment
@john Я ответил на свой вопрос. Все в порядке?   -  person Akib Azmain    schedule 25.09.2020


Ответы (1)


Создать класс, который ведет себя как поток, очень просто. Допустим, мы хотим создать такой класс с именем MyStream , определение класса будет таким простым, как:

#include <istream> // class "basic_iostream" is defined here

class MyStream : public std::basic_iostream<char> {
private:
    std::basic_streambuf buffer; // your streambuf object
public:
    MyStream() : std::basic_iostream<char>(&buffer) {} // note that ampersand
};

Конструктор вашего класса должен вызывать конструктор std::basic_iostream<char> с указателем на пользовательский объект std::basic_streambuf<char>. std::basic_streambuf — это просто класс шаблона, который определяет структуру буфера потока. Таким образом, вы должны получить свой собственный буфер потока. Вы можете получить его двумя способами:

  1. Из другого потока. В каждом потоке есть элемент rdbuf, который не принимает аргументов и возвращает указатель на используемый им буфер потока. Пример:
...
std::basic_streambuf* buffer = std::cout.rdbuf(); // take from std::cout
...
  1. Создайте свой собственный. Вы всегда можете создать класс буфера, производный от std::basic_streambuf<char>, и настроить его по своему усмотрению.

Теперь мы определили и реализовали класс MyStream, нам нужен буфер потока. Давайте выберем вариант 2 сверху и создадим собственный буфер потока и назовем его MyBuffer . Нам понадобится следующее:

  1. Конструктор для инициализации объекта.
  2. Непрерывный блок памяти для временного хранения вывода программы.
  3. Непрерывный блок памяти для временного хранения ввода пользователя (или чего-то другого).
  4. Метод overflow , который вызывается, когда память, выделенная для хранения выходных данных, заполнена.
  5. Метод underflow , который вызывается, когда программа считывает все входные данные и запрашивает дополнительные входные данные.
  6. Метод sync , который вызывается при сбросе вывода.

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

class MyBuffer : public std::basic_streambuf<char> {
private:
    char inbuf[10];
    char outbuf[10];

    int sync();
    int_type overflow(int_type ch);
    int_type underflow();
public:
    MyBuffer();
};

Здесь inbuf и outbuf — это два массива, которые будут хранить ввод и вывод соответственно. int_type — это специальный тип, похожий на char и созданный для поддержки нескольких типов символов, таких как char, wchar_t и т. д.

Прежде чем мы перейдем к реализации нашего класса буфера, нам нужно знать, как он будет работать.

Чтобы понять, как работают буферы, нам нужно знать, как работают массивы. Массивы — это не что иное, как указатели на непрерывную память. Когда мы объявляем массив char с двумя элементами, операционная система выделяет 2 * sizeof(char) памяти для нашей программы. Когда мы обращаемся к элементу массива с помощью array[n], он преобразуется в *(array + n), где n — номер индекса. Когда вы добавляете n к массиву, он переходит к следующему n * sizeof(<the_type_the_array_points_to>) (рис. 1). Если вы не знаете, что такое арифметика указателей, я бы порекомендовал вам изучить это, прежде чем продолжить. cplusplus.com имеет хорошая статья об указателях для начинающих.

             array    array + 1
               \        /
------------------------------------------
  |     |     | 'a' | 'b' |     |     |
------------------------------------------
    ...   105   106   107   108   ...
                 |     |
                 -------
                    |
            memory allocated by the operating system

                     figure 1: memory address of an array

Теперь, когда мы много знаем об указателях, давайте посмотрим, как работают буферы потока. Наш буфер содержит два массива inbuf и outbuf. Но как стандартная библиотека узнает, что ввод должен храниться в inbuf, а вывод должен храниться в outbuf? Итак, есть две области, называемые областью получения и областью ввода, которые являются областью ввода и вывода соответственно.

Область вывода задается следующими тремя указателями (рисунок 2):

  • pbase() или база путов: начало области путов
  • epptr() или конечный указатель: конец области ввода
  • pptr() или указатель: куда будет помещен следующий символ

На самом деле это функции, которые возвращают соответствующий указатель. Эти указатели устанавливаются setp(pbase, epptr) . После вызова этой функции pptr() устанавливается на pbase(). Чтобы изменить его, мы будем использовать pbump(n), который перемещает pptr() на n символов, n может быть положительным или отрицательным. Обратите внимание, что поток будет записывать в предыдущий блок памяти epptr(), но не epptr().

  pbase()                         pptr()                       epptr()
     |                              |                             |
------------------------------------------------------------------------
  | 'H' | 'e' | 'l' | 'l' | 'o'  |     |     |     |     |     |     |
------------------------------------------------------------------------
     |                                                      |
     --------------------------------------------------------
                                 |
                   allocated memory for the buffer

           figure 2: output buffer (put area) with sample data

Область получения задается следующими тремя указателями (рисунок 3):

  • eback() или конец назад, начало области получения
  • egptr() или конец указателя получения, конец области получения
  • gptr() или получить указатель, позицию, которая будет прочитана

Эти указатели устанавливаются функцией setg(eback, gptr, egptr). Обратите внимание, что поток будет читать предыдущий блок памяти egptr(), но не egptr().

  eback()                         gptr()                       egptr()
     |                              |                             |
------------------------------------------------------------------------
  | 'H' | 'e' | 'l' | 'l' | 'o'  | ' ' | 'C' | '+' | '+' |     |     |
------------------------------------------------------------------------
     |                                                      |
     --------------------------------------------------------
                                 |
                   allocated memory for the buffer

           figure 3: input buffer (get area) with sample data

Теперь, когда мы обсудили почти все, что нам нужно знать перед созданием пользовательского буфера потока, пришло время его реализовать! Мы попробуем реализовать наш потоковый буфер так, чтобы он работал как std::cout!

Начнем с конструктора:

MyBuffer() {
    setg(inbuf+4, inbuf+4, inbuf+4);
    setp(outbuf, outbuf+9);
}

Здесь мы устанавливаем все три указателя на одну позицию, что означает отсутствие читаемых символов, принудительно устанавливая underflow(), когда требуется ввод. Затем мы устанавливаем указатель put таким образом, чтобы поток мог записывать весь массив outbuf, кроме последнего элемента. Мы сохраним его для будущего использования.

Теперь давайте реализуем метод sync(), который вызывается при сбросе вывода:

int sync() {
    int return_code = 0;

    for (int i = 0; i < (pptr() - pbase()); i++) {
        if (std::putchar(outbuf[i]) == EOF) {
            return_code = EOF;
            break;
        }
    }

    pbump(pbase() - pptr());
    return return_code;
}

Это делает его работу очень легко. Сначала он определяет, сколько символов нужно напечатать, затем печатает один за другим и перемещает pptr() (помещает указатель). Он возвращает EOF или -1, если символ любой символ является EOF, 0 в противном случае.

Но что делать, если область ввода заполнена? Итак, нам нужен метод overflow(). Давайте реализуем это:

int_type overflow(int_type ch) {
    *pptr() = ch;
    pbump(1);

    return (sync() == EOF ? EOF : ch);
}

Ничего особенного, это просто помещает дополнительный символ в сохраненный последний элемент outbuf и перемещает pptr() (помещает указатель), а затем вызывает sync() . Он возвращает EOF, если sync() вернул EOF, иначе дополнительный символ.

Теперь все завершено, кроме обработки ввода. Давайте реализуем underflow() , который вызывается, когда все символы во входном буфере прочитаны:

int_type underflow() {
    int keep = std::max(long(4), (gptr() - eback()));
    std::memmove(inbuf + 4 - keep, gptr() - keep, keep);

    int ch, position = 4;
    while ((ch = std::getchar()) != EOF && position <= 10) {
        inbuf[position++] = char(ch);
        read++;
    }
    
    if (read == 0) return EOF;
    setg(inbuf - keep + 4, inbuf + 4 , inbuf + position);
    return *gptr();
}

Немного сложно понять. Давайте посмотрим, что здесь происходит. Во-первых, он вычисляет, сколько символов он должен сохранить в буфере (максимум 4), и сохраняет его в переменной keep. Затем он копирует последние keep числовых символов в начало буфера. Это сделано потому, что символы могут быть помещены обратно в буфер с помощью метода unget() std::basic_iostream . Программа может даже читать следующие символы, не извлекая их с помощью метода peek() из std::basic_iostream . После того, как последние несколько символов возвращены, он считывает новые символы, пока не достигнет конца входного буфера или не получит на входе EOF. Затем он возвращает EOF, если символы не прочитаны, в противном случае продолжается. Затем он перемещает все указатели на получение и возвращает первый прочитанный символ.

Поскольку наш потоковый буфер теперь реализован, мы можем настроить наш потоковой класс MyStream, чтобы он использовал наш потоковый буфер. Итак, мы меняем приватную переменную buffer:

...
private:
    MyBuffer buffer;
public:
...

Теперь вы можете протестировать свой собственный поток, он должен принимать входные данные и отображать вывод из терминала.


Обратите внимание, что этот поток и буфер могут обрабатывать только ввод и вывод на основе char. Ваш класс должен быть производным от соответствующего класса для обработки других типов ввода и вывода (например, std::basic_streambuf<wchar_t> для широких символов) и реализовывать функции-члены или метод, чтобы они могли обрабатывать этот тип символов.

person Akib Azmain    schedule 26.08.2020
comment
Удивительное объяснение. Тем не менее, одна небольшая рекомендация: ваш пример ввода ввода был бы лучше, если бы читаемые символы были справа от указателя, а не слева. - person Tanveer Badar; 05.10.2020