Встань у реки, смотри, как течет река; Ее не поймать ни в сеть, ни рукой. Она безымянна, ведь имя есть лишь у ее берегов; Забудь свое имя и стань рекой. Борис Гребенщиков
Библиотека ввода-вывода языка С++ — достаточно спорное явление. Но, так или иначе, она существует, иногда используется, и надо как-то с этим жить.
Типичные сценарии работы с потоком — порождение и преобразование. Порождение это, например, выдача в поток данных из сокета. Хороший пример преобразования — перекодирование (в base64, в другую кодировку, шифрование, архивирование). Ещё можно что-нибудь лишнее удалять (пробелы, комментарии), а что-нибудь нужное добавлять (разворачивать макросы). Но при ближайшем рассмотрении оказывается, что преобразование это частный случай порождения, когда данные «порождаются» не «из сокета», а на основе исходного потока. И в итоге, всё сводится к созданию потока со стандартным интерфейсом и своим собственным источником данных.
Постановка задачи проста и логична, разработчики библиотеки iostream, конечно, о ней догадывались и даже предприняли некоторые шаги… Нужно только понять, какие. Итак, задачка на reverse engineering: есть куча кода (реализация библиотеки), требуется понять, как он работает и что хотели сказать авторы.
Собственно, всё что они хотели сказать, они сказали самой библиотекой, но в ней слишком много сюжетных линий, они пересекаются удивительным образом и это может понять только компилятор, а нам нужен один маленький кусочек…
Искать смысл будем в несколько идеализированных исходниках из VC2005, искать честно: почти половину статьи составляет скопированный код iostream :) Главное — выкинуть лишнее и расставить оставшееся в нужном порядке.
ПРИМЕЧАНИЕ Конечный результат проверялся в VC2005 и в gcc 4.2. STLPort отличается не существенно, а публичные и защищённые методы вообще прописаны в стандарте и должны совпадать во всех реализациях. Идеализация — удалены некоторые незначимые мелочи… |
Конечно, я уже знаю «правильный ответ», и мог бы объяснить его как-нибудь короче, понятнее и систематичнее… Но мне кажется более интересным провести вас тем же путём, которым шёл я сам, по сторонам открываются потрясающие виды. Следуйте за мной.
И я тебе скажу в свою чреду: Иди за мной, и в вечные селенья Из этих мест тебя я приведу, И ты услышишь вопли исступленья И древних духов, бедствующих там, О новой смерти тщетные моленья; Данте Алигьери, перевод Михаила Лозинского
Начнём решать задачу с изучения обстановки.
Во-первых, istream это вообще не класс, а всего лишь тайпдеф.
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 нет ни второго конструктора ни второго параметра, в стандарте тоже. |
Оптимист начал бы разматывать 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, в реализацию которого мы тоже углубляться не будем, просто использует итератор.
Ну и хватит. Что у нас получилось:
Возможно, что-то мы пропустили, но некоторое понимание того, как работает basic_streambuf уже должно сложиться.
— Давайте отрежем Сусанину ногу. — Не надо, ребята, я вспомнил дорогу! фольклор
К сожалению, идея, заложенная в класс basic_streambuf, не выводится очевидным образом из его реализации, поэтому здесь мне не удастся ограничиться копированием в статью исходного кода; придётся и самому что-то написать.
Предлагаемая модель функционирования наследника basic_streambuf:
ПРИМЕЧАНИЕ По GoF, использованный паттерн проектирования называется Template Method; его же часто называют Non-Virtual Interface. Спасибо Андрею Солодовникову за напоминание названия паттерна и несколько других важных поправок. |
Для простоты буферизацию можно отключить, а большую часть «других ситуаций» игнорировать. Если в качестве буфера установить 0, все операции будут «выходить за край», то есть каждый раз будут вызываться обработчики; осталось только правильно их реализовать. Этим и займёмся, к буферам и прочему вернёмся позже.
Сначала — публичный интерфейс. Выбросим тайпдефы, работу с буферами, позиционирование, локализации и всё остальное, что нас сейчас не интересует:
// 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 в цикле должен всё время возвращать одно и то же.
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 в цикле должен последовательно прочитать все доступные символы.
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 в цикле должен последовательно прочитать все доступные символы кроме первого. Предназначен для использования в паре с 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 и sgetc.
Пытаются скопировать по переданному адресу заданное количество символов, возвращают количество реально скопированных.
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. |
Выявлены:
Примерно на десять минут работы. Вот такой полезный вспомогательный класс:
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. Что ещё нужно отметить:
В общем-то, с буфером даже проще. Да, нужно запомнить ещё несколько функций, но зато, как выясняется, они внесены в интерфейс 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 что-то успешно прочитала, то она заносит это в буфер. И, если это действительно так, 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 её нужно переопределить так чтобы она:
Примерно так:
// Определит потомок // Возвращает либо количество прочитанного, либо -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_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 и замечательно подходит для большинства применений.
Стандартный поточный интерфейс подразумевает последовательный доступ к данным, но, если поток способен на большее, 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); |
Что интересного можно сказать про всю эту конструкцию:
Почти всё, что говорилось про чтение верно и для записи. Ну, там, конечно, другие имена методов, но в целом то же самое. Единственное существенное отличие — нужен аналог для функции flush. Для этого предназначен метод pubsync, который вызывает переопределяемый метод sync:
virtual int sync(); |
Возвращаемое значение: -1 при ошибке, что-то другое — при успехе.
Естественно, basic_ostream вызывает его из своего метода flush.
ПРИМЕЧАНИЕ А вот зачем вызов pubsync/sync нужен в basic_istream — действительно не понятно :) |
В наше прогрессивное время сообщать о проблемах, возвращая -1, — это просто таки ретроградство. Было бы странно, если бы библиотека с таким количеством шаблонов не поддерживала исключения. Кончено, она их поддерживает. Но не самым очевидным способом.
Модель следующая:
Ну, и главное:
ПРИМЕЧАНИЕ По стандарту все перечисленные здесь методы расположены в классе basic_ios, Microsoft переместила их в ios_base и немного поменяла. Функциональность сохранена. |
Для стандартных потоков такая недоговорённость относительно типов выбрасываемых исключений имеет существенный минус. Ни одна реализация stl не рискнёт кидать какое-то «своё» исключение из кода fstream или stringstream — иначе рассчитывающий на него пользовательский код будет непереносимым и вся «стандартность» библиотеки вылетит в трубу. В результате никаких внятных сообщений об ошибках стандартные классы потоков предложить не могут: исключения использовать не получается, а других средств не предусмотрено.
Зато, если вы пишете свой поток, стандарт даёт зелёный свет: кидайте любые исключения, полная свобода.
Вообще-то, не очень. Во всяком случае, в VS2005 форматированный вывод в буфер при помощи sprintf работает примерно в три раза быстрее, чем stringstream, замена stringstream на собственный класс потока не даёт видимого эффекта.
Как было сказано в самом начале: «постановка задачи проста и логична»; и это действительно так, ничего нового я не придумал. В частности, разработчикам библиотеки boost похожие мысли тоже приходили в голову, и из них родилась Boost.Iostreams Library. Простая, понятная, удобная, работающая.
Если обстоятельства непреодолимой силы не принуждают вас создавать собственный велосипед, используйте boost. А всё, что было написано выше, можно забыть или даже не знать. Но лучше — знать и принимать к сведению.
Теперь девушки :)
Но и о потоках тоже не забывайте.