Сергей Холодилов
Встань у реки, смотри, как течет река;
Ее не поймать ни в сеть, ни рукой.
Она безымянна, ведь имя есть лишь у ее берегов;
Забудь свое имя и стань рекой.
Борис Гребенщиков
Библиотека ввода-вывода языка С++ - достаточно спорное явление. Но, так или иначе, она существует, иногда используется, и надо как-то с этим жить.
Типичные сценарии работы с потоком - порождение и преобразование. Порождение - это, например, выдача в поток данных из сокета. Хороший пример преобразования - перекодирование (в base64, в другую кодировку, шифрование, архивирование). Ещё можно что-нибудь лишнее удалять (пробелы, комментарии), а что-нибудь нужное добавлять (разворачивать макросы). Но при ближайшем рассмотрении оказывается, что преобразование - это частный случай порождения, когда данные "порождаются" не "из сокета", а на основе исходного потока. И, в итоге, всё сводится к созданию потока со стандартным интерфейсом и своим собственным источником данных.
Постановка задачи проста и логична, разработчики библиотеки iostream, конечно, о ней догадывались и даже предприняли некоторые шаги… Нужно только понять, какие. Итак, задачка на reverse engineering: есть куча кода (реализация библиотеки), требуется понять, как он работает и что хотели сказать авторы.
Собственно, всё, что они хотели сказать, они сказали самой библиотекой, но в ней слишком много сюжетных линий, которые пересекаются удивительным образом. Понять ее целиком может только компилятор, а нам нужен один маленький кусочек…
Искать смысл будем в несколько идеализированных исходниках из VC2005, искать честно: почти половину статьи составляет скопированный код iostream :) Главное - выкинуть лишнее и расставить оставшееся в нужном порядке.
ПРИМЕЧАНИЕ
Конечный результат проверялся в VC2005 и в gcc 4.2. STLPort отличается несущественно, а публичные и защищённые методы вообще прописаны в стандарте и должны совпадать во всех реализациях.
Идеализация - удалены некоторые незначимые мелочи… |
Конечно, я уже знаю "правильный ответ", и мог бы объяснить его как-нибудь короче, понятнее и систематичнее… Но мне кажется более интересным провести вас тем же путём, которым шёл я сам, по сторонам открываются потрясающие виды. Следуйте за мной.
Все круги istream
И я тебе скажу в свою чреду:
Иди за мной, и в вечные селенья
Из этих мест тебя я приведу,
И ты услышишь вопли исступленья
И древних духов, бедствующих там,
О новой смерти тщетные моленья;
Данте Алигьери,
перевод Михаила Лозинского
Начнём решать задачу с изучения обстановки.
Генеалогия
Во-первых, istream это вообще не класс, а всего лишь typedef.
typedef basic_istream<char, char_traits<char> > istream;
|
Реальный класс называется basic_istream:
template<class _Elem, class _Traits>
class basic_istream: virtual public basic_ios<_Elem, _Traits>
{ // control extractions from a stream buffer
|
Он унаследован от:
template<class _Elem, class _Traits>
class basic_ios: public ios_base
{ // base class for basic_istream/basic_ostream
|
А тот от:
class ios_base : public _Iosb<int>
{ // base class for ios
|
Ну и наконец:
template<class _Dummy>
class _Iosb
{ // define templatized bitmask/enumerated types, instantiate on demand
|
ПРИМЕЧАНИЕ
Класс _Iosb - Microsoft specific, такая особенность реализации, ни на что существенное не влияет. Остальные прописаны в стандарте. |
Обобщённые символы
В соответствии с идеологией STL, поток работает с обобщёнными символами. Конкретизация потока получает в параметрах шаблона класс символов и пачку методов для работы с ними. "Пачка методов" выглядит примерно так:
template<class _Elem>
struct char_traits
{
typedef _Elem char_type;
typedef long int_type;
typedef streampos pos_type;
typedef streamoff off_type;
typedef _Mbstatet state_type;
static void assign(_Elem& _Left, const _Elem& _Right);
static bool eq(const _Elem& _Left, const _Elem& _Right);
static bool lt(const _Elem& _Left, const _Elem& _Right);
static int compare(const _Elem *_First1, const _Elem *_First2, size_t _Count);
static size_t length(const _Elem *_First);
static _Elem* copy(_Elem *_First1, const _Elem *_First2, size_t _Count);
static const _Elem* find(const _Elem *_First, size_t _Count, const _Elem& _Ch);
static _Elem* move(_Elem *_First1, const _Elem *_First2, size_t _Count);
static _Elem* assign(_Elem *_First, size_t _Count, _Elem _Ch);
static _Elem to_char_type(const int_type& _Meta);
static int_type to_int_type(const _Elem& _Ch);
static bool eq_int_type(const int_type& _Left, const int_type& _Right);
static int_type eof();
static int_type not_eof(const int_type& _Meta);
};
|
И вместо стандартных функций и операторов ==, >, < реализация потока честно использует именно эти методы. Немного длиннее и выглядит странно, зато одинаково успешно работает с char и с wchar_t (для них сделаны явные специализации char_traits). Ещё можно делать вот такие бесполезные штуки:
#include <fstream>
namespace std
{
// если не определить свой char_traits, всё
// будет преобразовываться к int-у, дробные части потеряются
template<> struct char_traits<float>
{
typedef float _Elem;
typedef float char_type;
typedef float int_type;
... // сюда скопировать реализацию char_traits
};
typedef basic_fstream<float, std::char_traits<float> > ffstream;
}
int main()
{
std::ffstream fs("test", std::ios_base::out);
float f[10] = {1.1, 2.2, 3.3, -1, 5.5, 6.6, 7.7, 8.8, 9.9, 0};
fs.write(f, 10); // Ну, это понятно
fs << "test"; // Но и это тоже работает!
fs << 3.1416; // Угадайте, как работает вот это?
fs << 3.14f; // А если так?
fs.close();
}
|
ПРИМЕЧАНИЕ
Это код для VC2005, и он более-менее рабочий.
В gcc char_traits реализован более гибко: используемые типы задаются структурой _Char_types, которую можно явно специализировать для float. Этот код компилируется и запускается, но почему-то совсем не работает, не разбирался почему. |
Небольшая проблема заключается в существовании char_traits::eof(). Значение, являющееся признаком конца, не должно принадлежать char_type, иначе оно может неожиданно встретиться в середине файла. Именно для этого введён тип int_type: он должен включать в себя весь char_type и ещё хотя бы одно значение, которое можно будет объявить eof-ом.
В специализации char_traits для char сделано так:
typedef char _Elem;
typedef _Elem char_type;
typedef int int_type;
...
static int_type to_int_type(const _Elem& _Ch)
{ // convert character to metacharacter
return ((unsigned char)_Ch);
}
static int_type eof()
{ // return end-of-file metacharacter
return (EOF); // определён как -1 - С.Х.
}
|
В результате eof это -1, а нормальные символы всегда преобразуются к положительным int-ам. Приведённая реализация специализации char_traits для float всего этого не учитывает, из-за чего поток не совсем корректно реагирует на -1 на входе.
Источник данных
Через некоторое время становится ясно, что basic_istream вовсе не абстрактный базовый класс, в котором можно переопределить чисто виртуальный метод get, считывающий следующий символ. Вообще, во всей иерархии классов виртуальные - только деструкторы, других виртуальных методов не наблюдается.
Зато есть несколько не виртуальных get-ов, самый простой из них выглядит так:
// extract a metacharacter
int_type get()
{
int_type _Meta = 0;
ios_base::iostate _State = ios_base::goodbit;
_Chcount = 0;
const sentry _Ok(*this, true);
if (!_Ok)
_Meta = _Traits::eof(); // state not okay, return EOF
else
{
// state okay, extract a character
_TRY_IO_BEGIN
_Meta = _Myios::rdbuf()->sbumpc(); <-- Смотреть сюда!
if (_Traits::eq_int_type(_Traits::eof(), _Meta))
_State /= ios_base::eofbit / ios_base::failbit; // end of file
else
++_Chcount; // got a character, count it
_CATCH_IO_END
}
_Myios::setstate(_State);
return (_Meta);
}
|
Метод rdbuf определён в классе basic_ios:
template<class _Elem, class _Traits>
class basic_ios : public ios_base
{
public:
typedef basic_streambuf<_Elem, _Traits> _Mysb;
...
_Mysb* rdbuf() const
{
// return stream buffer pointer
return (_Mystrbuf);
}
private:
...
_Mysb *_Mystrbuf; // pointer to stream buffer
};
|
Для того чтобы окончательно заинтересоваться классом basic_streambuf осталось привести код основного конструктора класса basic_istream:
// construct from stream buffer pointer
explicit basic_istream(_Mysb *_Strbuf, bool _Isstd = false) : _Chcount(0)
{
_Myios::init(_Strbuf, _Isstd);
}
|
ПРИМЕЧАНИЕ
Почему "основного"? Реализация basic_istream в VC2005 содержит ещё один конструктор:
basic_istream(_Uninitialized)
{
ios_base::_Addstd(this);
}
Судя по вызову _Addstd, его хотели использовать для стандартных потоков cin/cout/cerr, однако использовать всё же не стали (см. файл cout.cpp в исходниках crt из VC2005). Булев параметр _Isstd, видимо, тоже предназначался для стандартных потоков, но тоже не используется. В STLPort нет ни второго конструктора, ни второго параметра, в стандарте тоже. |
Источник данных-II
Оптимист начал бы разматывать basic_streambuf с того, на чём остановились, то есть со sbumpc. Но это слишком ненадёжный путь. Вместо этого ещё немного посмотрим, откуда basic_istream берёт данные.
ПРИМЕЧАНИЕ
Для ясности и краткости из кода убраны проверки состояния, параметров и ещё некоторые мелочи. Если вам оно надо - обратитесь к первоисточнику. |
Более интересный метод get:
// get up to _Count characters into NTCS, stop before _Delim
_Myt& get(_Elem *_Str, streamsize _Count, _Elem _Delim)
{
ios_base::iostate _State = ios_base::goodbit;
_Chcount = 0;
// extract characters
int_type _Meta = _Myios::rdbuf()->sgetc(); <-- (1)
for (; 0 < --_Count; _Meta = _Myios::rdbuf()->snextc()) <-- (2)
if (_Traits::eq_int_type(_Traits::eof(), _Meta))
{ // end of file, quit
_State /= ios_base::eofbit;
break;
}
else if (_Traits::to_char_type(_Meta) == _Delim)
break; // got a delimiter, quit
else
{ // got a character, add it to string
*_Str++ = _Traits::to_char_type(_Meta);
++_Chcount;
}
_Myios::setstate(_Chcount == 0 ? _State / ios_base::failbit : _State);
*_Str = _Elem(); // add terminating null character
return (*this);
}
|
ПРИМЕЧАНИЕ
Обратите внимание на возвращаемое значение: это ссылка на себя. Чтобы получить количество прочитанных символов, нужно вызвать метод gcount. |
Метод read:
_Myt& read(_Elem *_Str, streamsize _Count)
{
return _Read_s(_Str, (size_t)-1, _Count);
}
|
_Read_s придумана программистами из Microsoft и реализована так:
// read up to _Count characters into buffer
_Myt& _Read_s(_Elem *_Str, size_t _Str_size, streamsize _Count)
{
ios_base::iostate _State = ios_base::goodbit;
_Chcount = 0; /-- вот тут
V
const streamsize _Num = _Myios::rdbuf()->_Sgetn_s(_Str, _Str_size, _Count);
_Chcount += _Num;
if (_Num != _Count)
_State /= ios_base::eofbit / ios_base::failbit; // short read
_Myios::setstate(_State);
return (*this);
}
|
Суффикс "_s" образован от слова "secure". Подразумевается, что, раз ей передаётся на один размер больше, она более безопасная, некоторые в это верят. _Sgetn_s - это тоже идея Microsoft, в стандартной реализации basic_streambuf её нет. Но это мы забегаем вперёд.
ПРИМЕЧАНИЕ
Я не имею в виду, что все _s-функции бесполезны, я все не смотрел. Но в данном случае… Функция принимает на вход два числа: размер буфера и число считываемых символов. Как вы думаете, что она делает? Правильно, выбирает меньшее из двух. Не вижу, почему этого не может сделать вызывающий код. Не понимаю, на что он вообще может рассчитывать, пытаясь прочитать больше, чем влезает. |
Метод рeek:
// return next character, unconsumed
int_type peek()
{
ios_base::iostate _State = ios_base::goodbit;
_Chcount = 0;
int_type _Meta = 0;
if (_Traits::eq_int_type(_Traits::eof(),
_Meta = _Myios::rdbuf()->sgetc())) <-- Ага!
_State /= ios_base::eofbit;
_Myios::setstate(_State);
return (_Meta);
}
|
Похожим образом устроены putback, unget, sync, seekg, и tellg: они просто передают управление соответствующим им sputbackc, sungetc, pubsync, pubseekpos, pubseekoff.
Один из встроенных операторов >>:
typedef istreambuf_iterator<_Elem, _Traits> _Iter;
typedef num_get<_Elem, _Iter> _Nget;
...
// extract an int
_Myt& operator>>(int& _Val)
{
ios_base::iostate _State = ios_base::goodbit;
long _Tmp = 0;
const _Nget& _Nget_fac = _USE(ios_base::getloc(), _Nget);
_Nget_fac.get(_Iter(_Myios::rdbuf()), _Iter(0), *this, _State, _Tmp);
if (_State & ios_base::failbit // _Tmp < INT_MIN // INT_MAX < _Tmp)
_State /= ios_base::failbit;
else
_Val = _Tmp;
_Myios::setstate(_State);
return (*this);
}
|
Мы не будем углубляться в реализацию istreambuf_iterator, тем более что он использует уже встречавшиеся sbumpc и sgetc. А num_get, в реализацию которого мы тоже углубляться не будем, просто использует итератор.
Ну и хватит. Что у нас получилось:
- sbumpc - считывает символ, переходит к следующему (используется в первом get).
- sgetc - считывает символ, не переходит к следующему (используется в peek и ещё много где).
- snextc - переходит к следующему и возвращает его (используется во втором get).
- _Sgetn_s - считывает строку символов указанной длины (используется в _Read_s).
- sputbackc, sungetc - возврат символа обратно в поток
- pubsync - непонятно что.
- pubseekpos, pubseekoff - изменение текущей позиции в потоке.
Возможно, что-то мы пропустили, но некоторое понимание того, как работает basic_streambuf уже должно сложиться.
По болотам basic_streambuf
- Давайте отрежем Сусанину ногу.
- Не надо, ребята, я вспомнил дорогу!
фольклор
К сожалению, идея, заложенная в класс basic_streambuf, не выводится очевидным образом из его реализации, поэтому здесь мне не удастся ограничиться копированием в статью исходного кода; придётся и самому что-то написать.
Предлагаемая модель функционирования наследника basic_streambuf:
- Есть некоторый буфер "данных для чтения". Есть его начало, конец, указатель на текущий символ.
- Операции чтения получают данные из этого буфера, при этом сдвигают указатель вперёд. Операции чтения-без-сдвига - соответственно, не сдвигают. Операции putback/ungetc сдвигают указатель обратно.
- Все эти операции могут неожиданно дойти до какого-то края буфера. В этом случае вызывается обработчик для соответствующего типа выхода за пределы буфера.
- Обработчики - защищённые виртуальные методы, публичных виртуальных методов нет (кроме деструктора, но он не вполне "метод" на мой взгляд).
- И для записи всё то же самое.
- И ещё есть несколько защищённых виртуальных методов для разных других ситуаций.
ПРИМЕЧАНИЕ
По GoF, использованный паттерн проектирования называется Template Method; его же часто называют Non-Virtual Interface.
Спасибо Андрею Солодовникову за напоминание названия паттерна и несколько других важных поправок. |
Для простоты буферизацию можно отключить, а большую часть "других ситуаций" игнорировать. Если в качестве буфера установить 0, все операции будут "выходить за край", то есть каждый раз будут вызываться обработчики; осталось только правильно их реализовать. Этим и займёмся, к буферам и прочему вернёмся позже.
Простое чтение, методы
Сначала - публичный интерфейс. Выбросим typedef-ы, работу с буферами, позиционирование, локализации и всё остальное, что нас сейчас не интересует:
// control read/write buffers
template<class _Elem, class _Traits>
class basic_streambuf
{
public:
...
// Get area:
int_type sgetc();
int_type sbumpc();
int_type snextc();
streamsize sgetn(_Elem *_Ptr, streamsize _Count);
// MS specific
streamsize _Sgetn_s(_Elem *_Ptr, size_t _Ptr_size, streamsize _Count);
...
};
|
ПРИМЕЧАНИЕ
Заодно мы выбросили putback, к нему тоже вернёмся позже.
Реализация putback без буферов - ненужный ручной труд, чреватый ошибками; при правильном использовании буферов всё получается само. |
sgetc
Возвращает текущий символ и оставляет указатель на месте. Вызов sgetc в цикле должен всё время возвращать одно и то же.
int_type sgetc()
{ // get a character and don't point past it
return (0 < _Gnavail() ? _Traits::to_int_type(*gptr()) : underflow());
}
|
На краю буфера вызывается underflow:
virtual int_type underflow();
|
Это первый переопределяемый метод.
sbumpc
Возвращает текущий символ и передвигает указатель вперёд. Вызов sbumpc в цикле должен последовательно прочитать все доступные символы.
int_type sbumpc()
{ // get a character and point past it
return (0 < _Gnavail() ? _Traits::to_int_type(*_Gninc()) : uflow());
}
|
Если буфер кончился, вызывается unflow:
virtual int_type uflow();
|
Это второй переопределяемый метод.
snextc
Передвигает указатель вперёд и возвращает новый текущий символ. Вызов snextc в цикле должен последовательно прочитать все доступные символы кроме первого. Предназначен для использования в паре с sgetc.
int_type snextc()
{ // point to next character and return it
return (1 < _Gnavail()
? _Traits::to_int_type(*_Gnpreinc())
: _Traits::eq_int_type(_Traits::eof(), sbumpc())
? _Traits::eof() : sgetc());
}
|
Логика работы:
- Если до конца буфера больше одного символа, то перейти к следующему и вернуть его.
- Иначе, вызвать sbumpc, если она вернула traits_type::eof(), вслед за ней вернуть traits_type::eof().
- Иначе вернуть результат sgetc.
Никаких обработчиков напрямую не вызывается, достаточно корректно реализовать sbumpc и sgetc.
sgetn, _Sgetn_s
Пытаются скопировать по переданному адресу заданное количество символов, возвращают количество реально скопированных.
streamsize sgetn(_Elem *_Ptr, streamsize _Count)
{ // get up to _Count characters into array beginning at _Ptr
return xsgetn(_Ptr, _Count);
}
streamsize _Sgetn_s(_Elem *_Ptr, size_t _Ptr_size, streamsize _Count)
{ // get up to _Count characters into array beginning at _Ptr
return _Xsgetn_s(_Ptr, _Ptr_size, _Count);
}
|
Вызывают xsgetn и _Xsgetn_s соответственно:
virtual streamsize xsgetn(_Elem* _Ptr, streamsize _Count)
{ // get _Count characters from stream
// assume the destination buffer is large enough
return _Xsgetn_s(_Ptr, (size_t)-1, _Count);
}
virtual streamsize _Xsgetn_s(_Elem* _Ptr, size_t _Ptr_size, streamsize _Count)
{
...
}
|
Для корректной работы стандартных реализаций достаточно sbumpc, ничего нового они не вызывают. Переопределять имеет смысл, только если можно увеличить эффективность, мы не будем этим заниматься.
СОВЕТ
Обратите внимание: реализация basic_istream от Microsoft не вызывает sgetn вообще, а значит, и ваша эффективная xsgetn практически не принесет пользы. Придётся переопределить _Xsgetn_s, специально для MS. |
Простое чтение, обработчики
Выявлены:
- underflow;
- uflow;
- xsgetn, _Xsgetn_s - переопределять не обязательно.
Примерно на десять минут работы. Вот такой полезный вспомогательный класс:
template <typename ch, typename tr>
class basic_symbol_istreambuf: public std::basic_streambuf<ch, tr>
{
public:
typedef std::basic_streambuf<ch, tr> base;
// Иначе их не видит gcc
typedef typename base::int_type int_type;
typedef typename base::traits_type traits_type;
basic_symbol_istreambuf() : _ready(false)
{
// Отказываемся от буферов
setg(0, 0, 0);
setp(0, 0);
}
protected:
// Иначе их не видит gcc
using base::setg;
using base::setp;
// Определит потомок
// Возвращает либо traits_type::eof(), либо traits_type::to_int_type(c)
virtual int_type readChar() = 0;
// Реализация underflow
virtual int_type underflow()
{
if (_ready)
{
// Текущий символ уже прочитан
return _char;
}
_char = readChar();
_ready = true;
return _char;
}
// Реализация uflow
virtual int_type uflow()
{
if (_ready)
{
// Текущий символ уже прочитан
if (_char != traits_type::eof())
{
// И он не последний - нужно "перейти к следующему"
_ready = false;
}
return _char;
}
// Текущий символ ещё не прочитан
return readChar();
}
private:
int_type _char;
bool _ready;
};
|
А теперь:
class random_buf
: public basic_symbol_istreambuf<char, std::char_traits<char> >
{
public:
random_buf()
{
srand(time(0));
}
int readChar()
{
return traits_type::to_int_type(rand());
}
};
int main()
{
random_buf rb;
std::istream st(&rb);
std::string c;
st >> c; // Читает до первого пробельного символа
std::cout << c << '\n';
}
|
Вуаля! Создание потока выглядит немного неуклюже, но программа работает.
Через буферы к звёздам
With our full crew a-board
And our trust in the Lord
We're comin' in on a wing and a prayer
Harold Adamson
Для работы с буфером данных-для-чтения нам доступны следующие функции:
protected:
void setg(_Elem *_First, _Elem *_Next, _Elem *_Last); // Инициализация
_Elem *eback() const; // Начало буфера
_Elem *gptr() const; // Текущий символ
_Elem *egptr() const; // Конец
void gbump(int _Off); // Сдвигает позицию текущего символа
|
Имена функций - это головоломка:
* "g" в setg, gptr, egptr и gbump - от "get"
* "e" в eback и egptr - от "end"
Идея в том, что у буфера есть "текущее положение" и два способа передвижения: вперёд и назад. Передвижение любым из способов когда-нибудь упирается в край буфера, соответственно, у буфера есть два конца: конец для движения вперёд и конец для движения назад; egptr и eback. "Начало" - в текущей точке, здесь-и-сейчас.
Логика образования имён этих функций была открыта мне Николаем Меркиным, спасибо ему. |
Можно считать, что setg присваивает значение указателям, значения которых возвращают eback, gptr и egptr. Что ещё нужно отметить:
- setg можно вызывать сколько угодно раз.
- За выделением-освобождением памяти нужно следить самостоятельно.
Хорошие новости
В общем-то, с буфером даже проще. Да, нужно запомнить ещё несколько функций, но зато, как выясняется, они внесены в интерфейс basic_streambuf не случайно, он был рассчитан именно на такое использование.
Например, обработчик uflow имеет стандартную реализацию, вот она:
virtual int_type uflow()
{ // get a character from stream, point past it
return (_Traits::eq_int_type(_Traits::eof(), underflow())
? _Traits::eof() : _Traits::to_int_type(*_Gninc()));
}
|
Логика работы:
- Вызвать underflow.
- Если underflow вернула traits_type::eof(), тоже вернуть traits_type::eof().
- Иначе вернуть первый элемент буфера и передвинуть указатель вперёд.
Здесь неявно подразумевается, что если underflow что-то успешно прочитала, то она заносит это в буфер. И если это действительно так, uflow можно не переопределять, стандартная прекрасно работает.
Другой пример - реализация putback/unget. В basic_streambuf им соответствуют sputbackc и sungetc:
int_type sputbackc(_Elem _Ch)
{ // put back _Ch
return (gptr() != 0 && eback() < gptr() && _Traits::eq(_Ch, gptr()[-1])
? _Traits::to_int_type(*_Gndec())
: pbackfail(_Traits::to_int_type(_Ch)));
}
int_type sungetc()
{ // back up one position
return (gptr() != 0 && eback() < gptr()
? _Traits::to_int_type(*_Gndec()) : pbackfail());
}
|
Переопределяемый обработчик выхода за пределы - pbackfail:
virtual int_type pbackfail(int_type c = traits_type::eof());
|
Но стандартная реализация вполне справляется без него до тех пор, пока:
- Позиция текущего символа не сдвинулась на передний край буфера.
- Символ, "возвращаемый" в поток, совпадает с предыдущим прочитанным символом в буфере.
Надо ли разрешать пользователю больше - вопрос философский, если не надо, то и делать ничего не придётся.
Код
Из двух обязательных для переопределения функций осталась одна - underflow. В классе basic_buffered_istreambuf её нужно переопределить так, чтобы она:
- вызывала переопределяемую потомком функцию readData для чтения очередного куска данных;
- записывала прочитанное в буфер;
- вызывала setg и устанавливала указатели в правильное место;
- возвращала первый из прочтённых символов.
Примерно так:
// Определит потомок
// Возвращает либо количество прочитанного, либо -1
virtual int readData(char_type* buffer, size_t length) = 0;
// Реализация underflow
virtual int_type underflow()
{
// читаем новую порцию
int symbols_read = readData(_buffer, buffer_size);
if (symbols_read <= 0)
{
// не вполне удачно
setg(_buffer, _buffer, _buffer);
return traits_type::eof();
}
// удачно!
setg(_buffer, _buffer, _buffer + symbols_read);
// возвращаем текущий символ, не сдвигая указатель
return traits_type::to_int_type(*egptr());
}
|
Недостаток этого решения в том, что сразу после вызова такой underflow перестаёт работать стандартная реализация putback: некуда "отступить", текущий элемент находится в самом начале буфера. Это неприятно, пусть мы и ограничиваем глубину putback, но хочется, чтобы он всегда более-менее работал.
Модифицированная версия (а заодно буфер засунули в вектор, по морально-этическим соображениям):
template <typename ch, typename tr>
class basic_buffered_istreambuf: public std::basic_streambuf<ch, tr>
{
public:
typedef std::basic_streambuf<ch, tr> base;
// Иначе их не видит gcc
typedef typename base::int_type int_type;
typedef typename base::traits_type traits_type;
typedef typename base::char_type char_type;
basic_buffered_istreambuf(size_t size = 512, size_t back = 10)
: buffer_size(size), back_size(back)
{
_buffer.resize(back_size + buffer_size);
setg(0, 0, 0);
setp(0, 0);
}
protected:
// Иначе их не видит gcc
using base::setg;
using base::setp;
using base::eback;
using base::gptr;
using base::egptr;
// Определит потомок
// Возвращает либо количество прочитанного, либо -1
virtual int readData(char_type* buffer, size_t length) = 0;
// Реализация underflow
virtual int_type underflow()
{
size_t offset = 0;
if (eback() != egptr())
{
// обеспечиваем себе putback
// глубина не больше, чем:
// -- количество прочитанных символов
// -- константа back_size
offset = std::min<size_t>(back_size, egptr() - eback());
memmove(&_buffer[0], eback() - offset, offset);
}
// читаем новую порцию
int symbols_read = readData(&_buffer[offset], buffer_size);
if (symbols_readed <= 0)
{
// не вполне удачно
base::setg(&_buffer[0], &_buffer[offset], &_buffer[offset]);
return traits_type::eof();
}
// удачно!
base::setg(&_buffer[0],
&_buffer[offset],
&_buffer[offset + symbols_read]);
// возвращаем текущий символ не сдвигая указатель
return traits_type::to_int_type(*gptr());
}
private:
std::vector<char_type> _buffer;
const size_t buffer_size;
const size_t back_size;
};
|
Использовать так же, как версию без буфера.
СОВЕТ
Кстати, basic_symbol_istreambuf полезно переписать с использованием буфера, для этого надо сделать buffer_size равным 1. Из плюсов: не нужно будет переопределять uflow, бесплатно заработает putback/unget. |
… девушки?
Потому, потому что мы пилоты,
Небо наш, небо наш родимый дом...
Соломон Фогельсон
Наступило "потом", в общем-то, статья закончена. Но некоторые пропущенные мелочи хочется упомянуть, некоторые перспективы - продемонстрировать, коротко, в режиме "предупреждённый - вооружён". Девушки подождут ещё пять минут.
basic_streambuf::in_avail
Это последняя функция, относящаяся к чтению, она используется в basic_istream::readsome и возвращает количество символов, которые можно прочитать без задержек.
streamsize in_avail()
{ // return count of buffered input characters
streamsize _Res = _Gnavail();
return (0 < _Res ? _Res : showmanyc());
}
|
Она возвращает разницу между egptr и eback; если буфер отключен или пуст, возвращает результат вызова showmanyc.
virtual streamsize showmanyc();
|
ПРИМЕЧАНИЕ
Из стандарта: The morphemes of showmanyc are "es-how-many-see", not "show-manic".
Вы угадали, именно ради этого забавного комментария описание showmanic включено в статью :) |
Что может возвращать showmanyc:
- Неотрицательное число. Это количество символов, которое мы обещаем прочитать без задержек. Если 0 - ничего не обещаем.
- -1. Это значит, что любое чтение вернёт traits_type::eof().
Реализация по умолчанию возвращает 0 и замечательно подходит для большинства применений.
Управление позицией
Стандартный поточный интерфейс подразумевает последовательный доступ к данным, но, если поток способен на большее, iostream позволяет ему проявить себя. Для произвольного доступа предназначены методы pubseekoff и pubseekpos:
pos_type pubseekoff(off_type _Off, ios_base::seekdir _Way,
ios_base::openmode _Mode = ios_base::in / ios_base::out)
{ // change position by _Off, according to _Way, _Mode
return (seekoff(_Off, _Way, _Mode));
}
pos_type pubseekpos(pos_type _Pos,
ios_base::openmode _Mode = ios_base::in / ios_base::out)
{ // change position to _Pos, according to _Mode
return (seekpos(_Pos, _Mode));
}
|
Они реализованы через seekoff и seekpos:
virtual pos_type seekoff(off_type off, ios_base::seekdir way,
ios_base::openmode mode);
virtual pos_type seekpos(pos_type pos, ios_base::openmode mode);
|
Что интересного можно сказать про всю эту конструкцию:
- Поскольку это не банальный C, понятие "текущая позиция" инкапсулирует класс traits_type::pos_type, и это честный класс, с определёнными в стандарте методами и операторами; но интересного в нём ничего нет. Несколько упрощая, можно сказать, что это обертка над 64-х разрядным числом.
- Методы, заканчивающиеся на "off" ведут себя как обычные функции установки текущей позиции в файле: устанавливают позицию относительно некоторой точки, точка задаётся параметром "way". Методы, заканчивающиеся на "pos", устанавливают абсолютную позицию.
- Параметр "mode" указывает, в котором потоке менять позицию: в потоке чтения или в потоке записи.
- Реализация по умолчанию возвращает ошибку.
Запись
Почти всё, что говорилось про чтение, верно и для записи. Ну, там, конечно, другие имена методов, но в целом то же самое. Единственное существенное отличие - нужен аналог для функции flush. Для этого предназначен метод pubsync, который вызывает переопределяемый метод sync:
Возвращаемое значение: -1 при ошибке, что-то другое - при успехе.
Естественно, basic_ostream вызывает его из своего метода flush.
ПРИМЕЧАНИЕ
А вот зачем вызов pubsync/sync нужен в basic_istream - действительно непонятно :) |
Исключения
В наше прогрессивное время сообщать о проблемах, возвращая -1, - это просто-таки ретроградство. Было бы странно, если бы библиотека с таким количеством шаблонов не поддерживала исключения. Конечно, она их поддерживает. Но не самым очевидным способом.
Модель следующая:
- У потока есть состояние. Там выставляются флажки badbit, eofbit и т.п. Текущее состояние возвращается методом rdstate(), устанавливается методами clear() и setstate().
- Ещё у потока есть "маска исключений". Возвращается методом exceptions(), устанавливается таким же методом, только с параметром.
- Если в какой-то момент так получилось, что (rdstate() & exceptions() != 0), генерируется исключение типа ios_base::failure. Сверка должна происходить как при изменении состояния, так и при изменении маски.
Ну, и главное:
- Если переопределённый пользователем метод basic_streambuf сгенерировал исключение, basic_istream должен его поймать, установить своё состояние в failbit и, если failbit присутствует в маске исключений, сгенерировать перехваченное исключение заново (а не ios_base::failure, как в обычной ситуации). Если failbit не присутствует в маске исключений, то исключение просто подавляется.
ПРИМЕЧАНИЕ
По стандарту все перечисленные здесь методы расположены в классе basic_ios, в Microsoft переместили их в ios_base и немного поменяли. Функциональность сохранена. |
Для стандартных потоков такая недоговорённость относительно типов исключений имеет существенный минус. Ни одна реализация stl не рискнёт генерировать какое-то "своё" исключение из кода fstream или stringstream - иначе рассчитывающий на него пользовательский код будет непереносимым, и вся "стандартность" библиотеки вылетит в трубу. В результате никаких внятных сообщений об ошибках стандартные классы потоков предложить не могут: исключения использовать не получается, а других средств не предусмотрено.
Зато, если вы пишете свой поток, стандарт даёт зелёный свет: кидайте любые исключения, полная свобода.
Производительность
Вообще-то, не очень. Во всяком случае, в VS2005 форматированный вывод в буфер при помощи sprintf работает примерно в три раза быстрее, чем stringstream. Замена stringstream на собственный класс потока не даёт видимого эффекта.
Boost it
Как было сказано в самом начале: "постановка задачи проста и логична"; и это действительно так, ничего нового я не придумал. В частности, разработчикам библиотеки boost похожие мысли тоже приходили в голову, и из них родилась Boost.Iostreams Library. Простая, понятная, удобная, работающая.
Если обстоятельства непреодолимой силы не принуждают вас создавать собственный велосипед, используйте boost. А всё, что было написано выше, можно забыть или даже не знать. Но лучше - знать и принимать к сведению.
Всё!
Теперь девушки :)
Но и о потоках тоже не забывайте.
Ссылки по теме