В статье на примерах объясняется, как реализовать поддержку потокового ввода-вывода из стандартной библиотеки (<iostream>) для своих классов.
В тексте статьи будет часто встречаться слово «поток», что означает именно поток ввода-вывода ((i/o)stream), но не поток выполнения (thread). Потоки выполнения в статье не рассматриваются.
Потоки из стандартной библиотеки — мощный инструмент. Аргументом функции можно указать поток, и это обеспечивает ее универсальность: она может работать как со стандартными файлами (fstream) и консолью (cin/cout), так и с сокетами и COM-портами, если найти соответствующую библиотеку.
Однако не всегда можно найти готовую библиотеку, где подходящий функционал уже реализован, может даже вы разрабатываете собственную библиотеку со своими классами. Тогда возникает задача реализации интерфейса потоков своими силами.
При написании статьи для теста примеров использовался компилятор g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4), а также стандарт c++11. Для наглядности я использовал из него ключевое слово override, чтобы пометить переопределяемые методы базового класса, однако если его убрать (а еще nullptr на NULL заменить), то должно собраться и на более старых стандартах.
Все примеры также доступны на github: streambuf_examples.
Каждый класс, поддерживающий потоковый ввод-вывод, наследует классы std::istream (ввод), std::ostream (вывод) или std::iostream (ввод и вывод). Именно они обеспечивают возможность использования перегруженных операторов '<<' и '>>', форматирования вывода, преобразование чисел в строки и наоборот и т.д.
Однако непосредственное чтение или запись данных происходят не в нем, а в классе, наследующем std::streambuf. Сам по себе streambuf является лишь интерфейсом с набором виртуальных функций, которые надо переопределить в классе-наследнике и уже в них реализовать свою логику чтения/записи данных (именно так и сделано в классах std::filebuf и std::stringbuf для fstream и stringstream соответственно).
Кроме того streambuf реализует часть логики работы с буфером. Программисту остается лишь задать начало и конец буфера и реализовать обработчики событий его переполнения, опустошения, синхронизации и т.п.
При разработке собственных потоков наиболее сложной частью является реализация наследника std::streambuf. Производные классы от istream, ostream или iostream в простых случаях могут и вовсе отсутствовать.
В простом случае или когда производительность не играет важной роли, буферы могут быть не нужны. Тогда достаточно переопределить всего три виртуальные функции:
Здесь и далее описание функций взято с cppreference.com
Пожалуй, пока хватит текста. В качестве примера разберем фильтрующий поток, который будет пропускать только символы цифр и пробелы (чтобы числа можно было как-то разделять друг от друга), сами данные будем брать из другого потока.
Результат работы программы:
Основные моменты в коде уже прокомментированы, однако дополнительно стоит отметить, что для чтения важно реализовать обе функции — uflow и underflow, поскольку underflow может вызываться до uflow и даже несколько раз подряд. Если добавить в начало этих функций отладочный вывод, это можно увидеть наглядно, например, при чтении из потока в целочисленную переменную.
Также в коде вы могли заметить использование типа
Как я уже ранее говорил, streambuf уже реализует в себе часть логики работы с буфером и предоставляет доступ к 6-и указателям, по 3 указателя на входной и выходной буферы. Однако streambuf не реализует выделение памяти под буферы. Эта задача возлагается на программиста вместе с инициализацией буферных указателей.
Для входного буфера указатели следующие:
Наглядная иллюстрация с сайта mr-edd.co.uk
Также для управления указателями входного буфера служат следующие фукнции:
Указатели выходного буфера имеют схожие имена и назначение:
Еще одна наглядная иллюстрация с сайта mr-edd.co.uk
Управляющие функции выходного буфера также схожи:
На этом теория кончается, и мы переходим к практике.
В одном проекте мне понадобилось прозрачно делить поток на небольшие части, каждая из которых при выводе сопровождалась некоторым заголовком. Реализовал я это с помощью нового наследника
Внимательный читатель наверняка давно уже задумался: буфер буфером, но ведь его надо как-то сбрасывать и не только при переполнении, но и по требованию программиста (подобно тому, как это происходит при записи в файл).
Для этого и служит еще одна виртуальная функция int sync(). Обычно она вызывается как раз по требованию программиста, однако в примере выше мы также вызываем ее сами при переполнении буфера. Возвращаемое ею значение говорит об успешной синхронизации (0) или неудачной (-1), при неудаче поток приобретает невалидное состояние. Реализация по-умолчанию ничего не делает и просто возвращает 0 (успех).
Кстати о переполнении буфера. В примере для упрощения реализации
Вывод программы для блоков размером в 10 символов следующий:
С чтением все несколько сложнее, поэтому начнем с простого. В примере ниже с помощью потока реализовано простое последовательное чтение файла. Для получения данных из файла используем средства стандартной библиотеки языка Си.
Так как пример простой, он обладает рядом недостатков, основные из которых мы разберем далее.
Несмотря на то, что получившиеся в предыдущих разделах потоки уже можно использовать, их реализация является неполной. На практике могут возникнуть более сложные ситуации, требующие дополнительного функционала, про который и будет рассказано далее.
При работе с файлом может потребоваться перемещение позиции в файле в произвольное место. Как вы уже догадались, в примере выше это не реализовано: файл читается только в одном направлении, назад вернуться нельзя, только переоткрывать файл. Чтобы исправить этот существенный недостаток, нам потребуется переопределить следующие методы класса
В функциях, помимо первого аргумента (позиции или смещения), присутствуют еще два:
Теперь, вооружившись этим знанием, представим, как может выглядеть реализация перемещения по файлу в примере 3:
Пояснение: в поле
Выглядит довольно просто, но на самом деле всю сложность на себя берет функция
Функция пытается переместить указатель в файле на заданную позицию и заполнить весь наш буфер от начала до конца. Не очень производительно при любой операции заново заполнять буфер из файла, но в примере это сделано для упрощения реализации. Когда вы будете реализовывать вашего собственного наследника streambuf, вам наверняка будут известны тонкости работы с вашими данными, чтобы написать максимально эффективные функции позиционирования указателей.
Ну а мы идем далее.
Бывают алгоритмы, которые не требуют свободного перемещения в произвольное место потока, однако в процессе чтения и обработки они могут попросить вернуть несколько символов (обычно 1-3) назад в поток. Для этого у
Теперь реализуем наш
Как я говорил ранее, в этом примере производительность будет ужасная, т.к. практически при каждом вызове
Здесь просто представлен код, в котором добавлены правки, реализованные в предыдущих разделах, а также примеры использования этого функционала, с пояснениями:
Помимо возможностей, рассмотренных в статье, есть и другие. Некоторые довольно просто реализуются, другие нужны лишь в специфических случаях, поэтому подробно они не рассматриваются. Далее приведен перечень таких функций и краткое описание, зачем они нужны. Более подробное описание о них вы можете найти в официальной документации (ссылка есть в конце статьи).
Другие доступные для переопределения методы:
Также в ваших проектах может возникнуть ситуация, когда размер одного символа больше 1 байта. В этом случае следует наследоваться от шаблонного класса
Стандартная библиотека предлагает большой объем функций для гибкой и производительной реализации ваших собственных потоков. Однако помните, что реальная производительность всегда зависит именно от вашей реализации переопределенных методов для вашего конкретного случая.
В тексте статьи будет часто встречаться слово «поток», что означает именно поток ввода-вывода ((i/o)stream), но не поток выполнения (thread). Потоки выполнения в статье не рассматриваются.
Введение
Потоки из стандартной библиотеки — мощный инструмент. Аргументом функции можно указать поток, и это обеспечивает ее универсальность: она может работать как со стандартными файлами (fstream) и консолью (cin/cout), так и с сокетами и COM-портами, если найти соответствующую библиотеку.
Однако не всегда можно найти готовую библиотеку, где подходящий функционал уже реализован, может даже вы разрабатываете собственную библиотеку со своими классами. Тогда возникает задача реализации интерфейса потоков своими силами.
Используемое окружение
При написании статьи для теста примеров использовался компилятор g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4), а также стандарт c++11. Для наглядности я использовал из него ключевое слово override, чтобы пометить переопределяемые методы базового класса, однако если его убрать (а еще nullptr на NULL заменить), то должно собраться и на более старых стандартах.
Все примеры также доступны на github: streambuf_examples.
Содержание
- Как устроены потоки?
- Простые случаи — без буферизации
- Используем буферы
- Расширенные возможности
- Заключение
- Ссылки
Как устроены потоки?
Каждый класс, поддерживающий потоковый ввод-вывод, наследует классы std::istream (ввод), std::ostream (вывод) или std::iostream (ввод и вывод). Именно они обеспечивают возможность использования перегруженных операторов '<<' и '>>', форматирования вывода, преобразование чисел в строки и наоборот и т.д.
Однако непосредственное чтение или запись данных происходят не в нем, а в классе, наследующем std::streambuf. Сам по себе streambuf является лишь интерфейсом с набором виртуальных функций, которые надо переопределить в классе-наследнике и уже в них реализовать свою логику чтения/записи данных (именно так и сделано в классах std::filebuf и std::stringbuf для fstream и stringstream соответственно).
Кроме того streambuf реализует часть логики работы с буфером. Программисту остается лишь задать начало и конец буфера и реализовать обработчики событий его переполнения, опустошения, синхронизации и т.п.
При разработке собственных потоков наиболее сложной частью является реализация наследника std::streambuf. Производные классы от istream, ostream или iostream в простых случаях могут и вовсе отсутствовать.
Простые случаи — без буферизации
В простом случае или когда производительность не играет важной роли, буферы могут быть не нужны. Тогда достаточно переопределить всего три виртуальные функции:
- int overflow(int c) — вызывается при переполнении буфера. Аргументом является символ, который «не влез» в буфер.
Возвращаемое значение: в случае успеха, код записанного сивола, приведенный к типу int, иначе EOF.
Поведение по-умолчанию: всегда возвращает EOF.
- int underflow() — вызывается для получения текущего символа без перехода к следующему.
Возвращаемое значение: в случае успеха, код считанного символа, приведенный к типу int, иначе EOF.
Поведение по-умолчанию: если буфер доступен и есть несчитанные символы, возвращает символ на текущей позиции в буфере, иначе EOF.
- int uflow() — то же самое, что underflow, но в случае успеха сдвигает указатель буфера к следующему символу.
Возвращаемое значение: как в underflow.
Поведение по-умолчанию: вызывает underflow. Если результат успешен, сдвигает указатель буфера к следующему символу и возвращает результат вызова underflow, в случае неудачи возвращает EOF. Попытка сдвинуть указатель незаданного буфера окончится segmentation fault-ом, не забудьте переопределить это поведение, если не используете буфер!
Здесь и далее описание функций взято с cppreference.com
Пример 1 — фильтруем цифры
Пожалуй, пока хватит текста. В качестве примера разберем фильтрующий поток, который будет пропускать только символы цифр и пробелы (чтобы числа можно было как-то разделять друг от друга), сами данные будем брать из другого потока.
Код
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
class numfilterbuf : public streambuf {
private:
istream *in;
ostream *out;
int cur; //последнее считанное значение, используется в underflow()
protected:
/* функции записи в поток: */
virtual int overflow(int c) override {
if (c == traits_type::eof()){
return traits_type::eof();
}
char_type ch = static_cast<char_type>(c);
if (ch == ' ' || (ch >= '0' && ch <= '9')){ // отдаем пробелы и цифры
out->put(ch);
//если что-то не записалось, отдаем EOF
return out->good() ? ch : traits_type::eof();
}
return ch;
}
/* функции чтения из потока: */
//реализация по-умолчанию инкрементирует позицию указателя в буфере и вызывает segmentation fault
virtual int uflow() override {
int c = underflow();
cur = traits_type::eof(); //говорим underflow() считать следующий символ при следующем вызове
return c;
}
virtual int underflow() override {
if (cur != traits_type::eof()){
return cur;
}
// пока можем читать, читаем
while (in->good()){
cur = in->get();
if (cur == traits_type::eof()){
return traits_type::eof();
}
char_type ch = static_cast<char_type>(cur);
if (ch == ' ' || (ch >= '0' && ch <= '9')){ // отдаем только пробелы и цифры
return ch;
}
}
return traits_type::eof();
}
public:
numfilterbuf(istream &_in, ostream &_out)
: in(&_in), out(&_out), cur(traits_type::eof())
{}
};
int main(int argc, char **argv){
const char str1[] = "In 4 bytes contains 32 bits";
const char str2[] = "Unix time starts from Jan 1, 1970";
istringstream str(str1);
numfilterbuf buf(str, cout); // читать из stringstream, выводить в консоль
iostream numfilter(&buf); // таким образом обходимся без реализации своего наследника iostream
string val;
getline(numfilter, val);
numfilter.clear(); // сбросить невалидное состояние после EOF в процессе чтения из stringstream
cout << "Original: '" << str1 << "'" << endl;
cout << "Read from numfilter: '" << val << "'" << endl;
cout << "Original: '" << str2 << "'" << endl;
cout << "Written to numfilter: '";
numfilter << str2;
cout << "'" << endl;
return 0;
}
Результат работы программы:
Original: 'In 4 bytes contains 32 bits'
Read from numfilter: ' 4 32 '
Original: 'Unix time starts from Jan 1, 1970'
Written to numfilter: ' 1 1970'
Основные моменты в коде уже прокомментированы, однако дополнительно стоит отметить, что для чтения важно реализовать обе функции — uflow и underflow, поскольку underflow может вызываться до uflow и даже несколько раз подряд. Если добавить в начало этих функций отладочный вывод, это можно увидеть наглядно, например, при чтении из потока в целочисленную переменную.
Также в коде вы могли заметить использование типа
char_type
. Он определяется в классе streambuf и в нашем случае является алиасом к типу char
, т.е. однобайтовый символ. Подробнее об этом будет сказано в конце статьи.Используем буферы
Как я уже ранее говорил, streambuf уже реализует в себе часть логики работы с буфером и предоставляет доступ к 6-и указателям, по 3 указателя на входной и выходной буферы. Однако streambuf не реализует выделение памяти под буферы. Эта задача возлагается на программиста вместе с инициализацией буферных указателей.
Для входного буфера указатели следующие:
- eback() (end back pointer) — указатель на первый элемент буфера
- gptr() (get pointer) — указатель на на элемент буфера, который будет считан следующим
- egptr() (end get pointer) — указатель на элемент, следующий за последним элементом буфера. Когда
gptr
достигает его, это означает, что буфер исчерпан и его нужно снова заполнить
Наглядная иллюстрация с сайта mr-edd.co.uk
Также для управления указателями входного буфера служат следующие фукнции:
- setg(eback, gptr, egptr) — устанавливает значения соответствующих указателей
- gbump(offset) — сдвинуть указатель
gptr
наoffset
позиций. Фактически, после выполнения функцииgptr
примет значениеgptr + offset
Указатели выходного буфера имеют схожие имена и назначение:
- pbase() (put base pointer) — указатель на первый элемент буфера
- pptr() (put pointer) — указатель на на элемент буфера, который будет записан следующим
- epptr() (end put pointer) — указатель на элемент, следующий за последним элементом буфера.
Еще одна наглядная иллюстрация с сайта mr-edd.co.uk
Управляющие функции выходного буфера также схожи:
- setp(pbase, epptr) — устанавливает значения соответствующих указателей. Обратите внимание, что у
setp
только два аргумента, в отличие отsetg
. При инициализации указателей выходного буфераpptr
автоматически приравниваетсяpbase
(т.е. устанавливается на начало буфера) - pbump(offset) — сдвинуть указатель
pptr
наoffset
позиций. Фактически, после выполнения функцииpptr
примет значениеpptr + offset
На этом теория кончается, и мы переходим к практике.
Пример 2 — блочный вывод
В одном проекте мне понадобилось прозрачно делить поток на небольшие части, каждая из которых при выводе сопровождалась некоторым заголовком. Реализовал я это с помощью нового наследника
streambuf
. Мне показалось, что этот класс достаточно просто и понятно показывает простую работу с выходным буфером. Поэтому в следующем примере мы будем делить вывод на части и обрамлять каждую тегами <start>
и <end>
:Код
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
class blockoutputbuf : public streambuf {
private:
ostream *out;
vector<char_type> buffer;
string startb, endb;
protected:
virtual int overflow(int c) override {
if (out->good() && c != traits_type::eof()){
*pptr() = c; //тут нам пригодился 1 "лишний" символ, убранный в конструкторе
pbump(1); //смещаем указатель позиции буфера на реальный конец буфера
return sync() == 0 ? c : traits_type::eof();
}
return traits_type::eof();
}
virtual int sync() override {
if (pptr() == pbase()) //если буфер пуст, то и синхронизировать нечего
return 0;
ptrdiff_t sz = pptr() - pbase(); //вычисляем, сколько символов записано в буффер
//заворачиваем буфер в наш блок
*out << startb;
out->write(pbase(), sz);
*out << endb;
if (out->good()){
pbump(-sz); //при успехе перемещаем указатель позиции буфера в начало
return 0;
}
return -1;
}
public:
blockoutputbuf(ostream &_out, size_t _bufsize, string _startb, string _endb)
: out(&_out), buffer(_bufsize), startb(_startb), endb(_endb)
{
char_type *buf = buffer.data();
setp(buf, buf + (buffer.size() - 1)); // -1 для того, чтобы упростить реализацию overflow()
}
};
int main(int argc, char **argv){
const char str1[] = "In 4 bytes contains 32 bits";
const char str2[] = "Unix time starts from Jan 1, 1970";
blockoutputbuf buf(cout, 10, "<start>", "<end>\n");
ostream blockoutput(&buf);
cout << "Original: '" << str1 << "'" << endl;
cout << "Written to blockoutputbuf: '";
blockoutput << str1;
blockoutput.flush(); //"сбросить" то, что не отправлено на консоль из str1
cout << "'" << endl;
cout << "Original: '" << str2 << "'" << endl;
cout << "Written to blockoutputbuf: '";
blockoutput << str2;
blockoutput.flush();
cout << "'" << endl;
return 0;
}
Внимательный читатель наверняка давно уже задумался: буфер буфером, но ведь его надо как-то сбрасывать и не только при переполнении, но и по требованию программиста (подобно тому, как это происходит при записи в файл).
Для этого и служит еще одна виртуальная функция int sync(). Обычно она вызывается как раз по требованию программиста, однако в примере выше мы также вызываем ее сами при переполнении буфера. Возвращаемое ею значение говорит об успешной синхронизации (0) или неудачной (-1), при неудаче поток приобретает невалидное состояние. Реализация по-умолчанию ничего не делает и просто возвращает 0 (успех).
Кстати о переполнении буфера. В примере для упрощения реализации
overflow()
применен небольшой трюк: фактический размер буфера всегда на 1 элемент больше, чем «думает» streambuf. Это позволяет поместить переданный функции overflow
«не влезший» символ в буфер и не усложнять код его специфичной обработкой.Вывод программы для блоков размером в 10 символов следующий:
Вывод
Original: 'In 4 bytes contains 32 bits' Written to blockoutputbuf: '<start>In 4 bytes<end> <start> contains <end> <start>32 bits<end> ' Original: 'Unix time starts from Jan 1, 1970' Written to blockoutputbuf: '<start>Unix time <end> <start>starts fro<end> <start>m Jan 1, 1<end> <start>970<end> '
Пример 3 — буферизированный ввод из файла
С чтением все несколько сложнее, поэтому начнем с простого. В примере ниже с помощью потока реализовано простое последовательное чтение файла. Для получения данных из файла используем средства стандартной библиотеки языка Си.
Код
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstdlib>
using namespace std;
class cfilebuf : public streambuf {
private:
vector<char_type> buffer;
FILE *file;
protected:
virtual int underflow() override {
if (!file)
return traits_type::eof();
if (gptr() < egptr()) //если буфер не пуст, вернем текущий символ
return *gptr();
char_type *start = eback();
//читаем не больше символов, чем вмещает буфер
size_t rd = fread(start, sizeof(char_type), buffer.size(), file);
//указываем размер буфера не больше, чем было считано символов
setg(start, start, start + rd);
return rd > 0 ? *gptr() : traits_type::eof();
}
public:
cfilebuf(size_t _bufsize)
: buffer(_bufsize), file(nullptr)
{
char_type *start = buffer.data();
char_type *end = start + buffer.size();
setg(start, end, end); //устанавливаем eback = start, gptr = end, egptr = end
//т.к. gptr == egptr, буфер по факту пуст и будет заполнен при первой попытке чтения
}
~cfilebuf(){
close();
}
bool open(string fn){
close();
file = fopen(fn.c_str(), "r");
return file != nullptr;
}
void close(){
if (file){
fclose(file);
file = nullptr;
}
}
};
int main(int argc, char **argv){
cfilebuf buf(10);
istream in(&buf);
string line;
buf.open("file.txt");
while (getline(in, line)){
cout << line << endl;
}
return 0;
}
Так как пример простой, он обладает рядом недостатков, основные из которых мы разберем далее.
Расширенные возможности
Несмотря на то, что получившиеся в предыдущих разделах потоки уже можно использовать, их реализация является неполной. На практике могут возникнуть более сложные ситуации, требующие дополнительного функционала, про который и будет рассказано далее.
seekoff и seekpos для перемещения по файлу
При работе с файлом может потребоваться перемещение позиции в файле в произвольное место. Как вы уже догадались, в примере выше это не реализовано: файл читается только в одном направлении, назад вернуться нельзя, только переоткрывать файл. Чтобы исправить этот существенный недостаток, нам потребуется переопределить следующие методы класса
streambuf
:- streampos seekpos(streampos sp, openmode which) — вызывается при попытке перемещения в позицию, заданную абсоолютной величиной, т.е. позицией от начала последовательности.
Возвращаемое значение: в случае успеха, новая установленная позиция, иначе -1.
Поведение по-умолчанию: ничего не делает и возвращает -1.
- streampos seekoff(streamoff off, seekdir way, openmode which) — вызывается при попытке перемещения в позицию, заданную относительно некоторой точки отсчета, которая задается аргументом
way
.
Возвращаемое значение: в случае успеха, новая установленная абсолютная позиция, иначе -1.
Поведение по-умолчанию: ничего не делает и возвращает -1.
В функциях, помимо первого аргумента (позиции или смещения), присутствуют еще два:
- openmode — тип указателя, который необходимо передвинуть:
ios_base::in
(позиция чтения) иios_base::out
(позиция записи). Заметьте что аргумент является битовой маской: т.е. может содержать как одно из значений, так и сразу оба.
- seekdir — применяется при относительном сдвиге и указывает точку отсчета для перемещения указателя. Может принимать одно из трех значений:
ios_base::beg
(от начала потока),ios_base::cur
(от текущей позиции) илиios_base::end
(от конца потока).
Теперь, вооружившись этим знанием, представим, как может выглядеть реализация перемещения по файлу в примере 3:
virtual streampos seekpos(streampos sp, ios_base::openmode which) override {
if (!(which & ios_base::in))
return streampos(-1);
return fill_buffer_from(sp);
}
virtual streampos seekoff(streamoff off, ios_base::seekdir way, ios_base::openmode which) override {
if (!(which & ios_base::in))
return streampos(-1);
switch (way){
default:
case ios_base::beg: return fill_buffer_from(off, SEEK_SET);
case ios_base::cur: return fill_buffer_from(pos_base + gptr() - eback() + off, SEEK_SET); //учитываем позицию от начала в нашем буфере
case ios_base::end: return fill_buffer_from(off, SEEK_END);
}
}
Пояснение: в поле
pos_base
хранится смещение в файле, с которого данные были загружены в буфер.Выглядит довольно просто, но на самом деле всю сложность на себя берет функция
fill_buffer_from
. Ее реализация следующая:streampos fill_buffer_from(streampos newpos, int dir = SEEK_SET){
if (!file || fseek(file, newpos, dir) == -1)
return -1;
long pos = ftell(file);
if (pos < 0)
return -1;
pos_base = pos;
char_type *start = eback();
size_t rd = fread(start, sizeof(char_type), buffer.size(), file);
setg(start, start, start + rd);
return rd > 0 && pos_base >= 0 ? pos_base : streampos(-1);
}
Функция пытается переместить указатель в файле на заданную позицию и заполнить весь наш буфер от начала до конца. Не очень производительно при любой операции заново заполнять буфер из файла, но в примере это сделано для упрощения реализации. Когда вы будете реализовывать вашего собственного наследника streambuf, вам наверняка будут известны тонкости работы с вашими данными, чтобы написать максимально эффективные функции позиционирования указателей.
Ну а мы идем далее.
pbackfail — возврат прочитанных символов назад
Бывают алгоритмы, которые не требуют свободного перемещения в произвольное место потока, однако в процессе чтения и обработки они могут попросить вернуть несколько символов (обычно 1-3) назад в поток. Для этого у
istream
предусмотрены методы unget()
и putback(character)
. В классе streambuf
при совпадении возвращенного в поток символа с предыдущим в буфере никаких дополнительных вызовов не происходит. Однако если символы не совпали или указатель буфера в самом его начале, то вызывается функция, позволяющая обработать эту ситуацию:- int pbackfail(int c) — вызывается при несовпадении возвращаемого в поток символа
c
и символа, находящегося в буфере на предыдущей позиции (или она не существует).
Возвращаемое значение: код возвращенного в поток символа, приведенный к типу int, в случае неудачи — EOF.
Поведение по-умолчанию: ничего не делает и возвращает EOF.
Теперь реализуем наш
pbackfail
:virtual int pbackfail(int c) override {
//символ не совпал
if (pos_base <= 0 || gptr() > eback())
return traits_type::eof();
//загружаем в буфер данные, начиная с предыдущего символа
if (fill_buffer_from(pos_base - 1L) == -1)
return traits_type::eof();
if (*gptr() != c){
gbump(1);
return traits_type::eof();
}
return *gptr();
}
Как я говорил ранее, в этом примере производительность будет ужасная, т.к. практически при каждом вызове
pbackfail
данные будут заново читаться из файла в буфер ради всего одного символа — предыдущего. Но целю этой статьи является понимание принципа работы, а не соревнование в производительности реализаций.Пример 4 — чтение файла с позиционированием и возвратом символов
Здесь просто представлен код, в котором добавлены правки, реализованные в предыдущих разделах, а также примеры использования этого функционала, с пояснениями:
Код
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstdlib>
using namespace std;
class cfilebuf : public streambuf {
private:
vector<char_type> buffer;
FILE *file;
streampos pos_base; //позиция в файле для eback
streampos fill_buffer_from(streampos newpos, int dir = SEEK_SET) {
if (!file || fseek(file, newpos, dir) == -1)
return -1;
//запоминаем текущую позицию в файле для eback
long pos = ftell(file);
if (pos < 0)
return -1;
pos_base = pos;
char_type *start = eback();
//читаем не больше символов, чем вмещает буфер
size_t rd = fread(start, sizeof(char_type), buffer.size(), file);
//указываем размер буфера не больше, чем было считано символов
setg(start, start, start + rd);
return rd > 0 && pos_base >= 0 ? pos_base : streampos(-1);
}
protected:
virtual int underflow() override {
if (!file)
return traits_type::eof();
if (gptr() < egptr()) //если буфер не пуст, вернем текущий символ
return *gptr();
streampos pos;
if (pos_base < 0) { //если буфер еще не заполнялся, заполняем с начала
pos = fill_buffer_from(0);
}
else { //иначе заполняем со следующего несчитанного символа
pos = fill_buffer_from(pos_base + egptr() - eback());
}
return pos != streampos(-1) ? *gptr() : traits_type::eof();
}
//второй аргумент в нашем случае всегда содержит ios_base::in
//однако в общем случае может содержать и ios_base::out и даже сразу оба (побитовое ИЛИ)
virtual streampos seekpos(streampos sp, ios_base::openmode which) override {
if (!(which & ios_base::in))
return streampos(-1);
return fill_buffer_from(sp);
}
//обработка трех вариантов позиционирования: с начала, с текущей позиции и с конца
virtual streampos seekoff(streamoff off, ios_base::seekdir way, ios_base::openmode which) override {
if (!(which & ios_base::in))
return streampos(-1);
switch (way) {
default:
case ios_base::beg: return fill_buffer_from(off, SEEK_SET);
case ios_base::cur: return fill_buffer_from(pos_base + gptr() - eback() + off); //учитываем позицию от начала в нашем буфере
case ios_base::end: return fill_buffer_from(off, SEEK_END);
}
}
virtual int pbackfail(int c) override {
//когда gptr > eback, значит в буфере есть символ на нужной позиции,
//но он не совпал с переданным, запрещаем
if (pos_base <= 0 || gptr() > eback())
return traits_type::eof();
//загружаем в буфер данные, начиная с предыдущего символа
if (fill_buffer_from(pos_base - streampos(1L)) == streampos(-1))
return traits_type::eof();
if (*gptr() != c) {
gbump(1); //возвращаемся назад, неудачная операция
return traits_type::eof();
}
return *gptr();
}
public:
cfilebuf(size_t _bufsize)
: buffer(_bufsize), file(nullptr), pos_base(-1)
{
char_type *start = buffer.data();
char_type *end = start + buffer.size();
setg(start, end, end); //устанавливаем eback = start, gptr = end, egptr = end
}
~cfilebuf() {
close();
}
bool open(string fn) {
close();
file = fopen(fn.c_str(), "r");
return file != nullptr;
}
void close() {
if (file) {
fclose(file);
file = nullptr;
}
}
};
void read_to_end(istream &in) {
string line;
while (getline(in, line)) {
cout << line << endl;
}
}
int main(int argc, char **argv) {
cfilebuf buf(10);
istream in(&buf);
buf.open("file.txt");
read_to_end(in);
in.clear(); //очистить невалидное состояние после конца файла
cout << endl << endl << "Read last 6 symbols:" << endl;
in.seekg(-5, ios_base::end); //передвинем позицию так, чтобы можно было считать 5 последних символов
in.seekg(-1, ios_base::cur); //а лучше 6, чтобы слово целиком влезло :)
read_to_end(in);
in.clear();
cout << endl << endl << "Read all again:" << endl;
in.seekg(0);
read_to_end(in);
in.clear();
in.seekg(2); //заставляем наш буфер начинаться с 3-го символа в файле (чтобы в буфере не было первых 2-ух)
in.get();
in.putback('b');
in.putback('a'); //без pbackfail() этот код не сработал бы и привел бы поток в невалидное состояние
in.putback('H');
string word;
in >> word;
cout << endl << endl << "Read word after putback(): " << word << endl;
return 0;
}
Другие возможности
Помимо возможностей, рассмотренных в статье, есть и другие. Некоторые довольно просто реализуются, другие нужны лишь в специфических случаях, поэтому подробно они не рассматриваются. Далее приведен перечень таких функций и краткое описание, зачем они нужны. Более подробное описание о них вы можете найти в официальной документации (ссылка есть в конце статьи).
Другие доступные для переопределения методы:
- imbue() — переопределение этой функции позволяет работать с различными локалями для преобразования читаемых и записываемых символов.
- setbuf() — позволяет использовать пользовательский буфер, вместо встроенного. По-умолчанию эта функция ничего не делает, но вы можете добавить в своей реализации такую возможность.
- showmanyc() — позволяет сообщить использующей функции, сколько символов еще можно прочесть из потока до блокировки. По-умолчанию возвращает 0 (т.е. нет информации о количестве символов).
- xsgetn() и xsputn() — пара методов для чтения/записи цельных блоков данных, по функциональности схожи с
fread
иfwrite
. Если вдруг для блочного чтения или записи в вашем случае можно реализовать более эффективный алгоритм, чем посимвольная обработка, то эти методы для вас.
Также в ваших проектах может возникнуть ситуация, когда размер одного символа больше 1 байта. В этом случае следует наследоваться от шаблонного класса
basic_streambuf
и использовать нужный вам тип символа. В реализации вам помогут такие алиасы типов, как char_type
, int_type
, pos_type
и т.д. Использовать их предпочтительнее, так как они всегда соответствуют тем типам, с которыми работает библиотечная реализация streambuf
.Заключение
Стандартная библиотека предлагает большой объем функций для гибкой и производительной реализации ваших собственных потоков. Однако помните, что реальная производительность всегда зависит именно от вашей реализации переопределенных методов для вашего конкретного случая.
Ссылки
- https://github.com/iassasin/streambuf_examples — все примеры из статьи на гитхабе;
- http://www.cplusplus.com/reference/streambuf/streambuf/ — официальная документация по streambuf;
- http://www.mr-edd.co.uk/blog/beginners_guide_streambuf — англоязычная статья, из которой были позаимствованны иллюстрации и идеи для некоторых примеров.
Поделиться с друзьями
Комментарии (8)
degs
17.04.2017 17:49Хорошо но поздновато. Такому тьюториалу цены бы не было лет 15 назад.
iassasin
17.04.2017 18:35Мне потребовалось год назад, а в рунете не смог найти никаких толковых статей. На английском нашел пару, но очень не хватало рабочих примеров и объяснения некоторых тонкостей. Оба этих недостатка постарался исправить в своей статье.
Кстати, почему поздновато? Сейчас используют что-то другое с подобным функционалом?degs
17.04.2017 19:17Нет, просто сейчас как то не так актуально стало. Раньше я действительно помню разные люди решали эту задачу параллельно и независимо, а сейчас ажиотаж просто спал. И вы очень верно подметили, я тоже помню это ощущуние, информация вроде есть, но катастрофически не хватает примеров и тонкостей. Как раз у вас с этим все хорошо.
maydjin
std::isspace, std::isdigit
apro
Разве по крайней мере
isspace
не зависит от локали,т.е. поведение твоего код может непредсказуемо меняться
в зависимости от глобальной переменной?
pfemidi
isdigit да, а вот isspace выловит не только пробел, но и табуляцию, и перевод строки, и form feed. А автор, насколько я понял, хочет ловить именно пробелы. Хотя isspace и более корректен.
iassasin
Да, в примере предполагалось ловить только пробелы. Но можно и указанные функции применить — суть примера все-равно не в них.