В статье на примерах объясняется, как реализовать поддержку потокового ввода-вывода из стандартной библиотеки (<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 в простых случаях могут и вовсе отсутствовать.

Простые случаи — без буферизации


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

  • 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.

Заключение


Стандартная библиотека предлагает большой объем функций для гибкой и производительной реализации ваших собственных потоков. Однако помните, что реальная производительность всегда зависит именно от вашей реализации переопределенных методов для вашего конкретного случая.

Ссылки


Поделиться с друзьями
-->

Комментарии (8)


  1. maydjin
    15.04.2017 23:33
    +4

    ch == ' ' || (ch >= '0' && ch <= '9')

    std::isspace, std::isdigit


    1. apro
      16.04.2017 03:28

      std::isspace, std::isdigit

      Разве по крайней мере isspace не зависит от локали,
      т.е. поведение твоего код может непредсказуемо меняться
      в зависимости от глобальной переменной?


    1. pfemidi
      16.04.2017 19:20

      isdigit да, а вот isspace выловит не только пробел, но и табуляцию, и перевод строки, и form feed. А автор, насколько я понял, хочет ловить именно пробелы. Хотя isspace и более корректен.


      1. iassasin
        16.04.2017 19:28

        Да, в примере предполагалось ловить только пробелы. Но можно и указанные функции применить — суть примера все-равно не в них.


  1. degs
    17.04.2017 17:49

    Хорошо но поздновато. Такому тьюториалу цены бы не было лет 15 назад.


    1. iassasin
      17.04.2017 18:35

      Мне потребовалось год назад, а в рунете не смог найти никаких толковых статей. На английском нашел пару, но очень не хватало рабочих примеров и объяснения некоторых тонкостей. Оба этих недостатка постарался исправить в своей статье.

      Кстати, почему поздновато? Сейчас используют что-то другое с подобным функционалом?


      1. degs
        17.04.2017 19:17

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


      1. oYASo
        24.04.2017 02:30
        -1

        Да отличный туториал, всегда бы так!