На этом шаге мы рассмотрим особенности конструирования собственных операторов ввода-вывода.
Приведенные на предыдущих шагах реализации операторов ввода-вывода поручали основную работу готовым операторам форматированного ввода-вывода. Иначе говоря, операторы << и >> реализовывались в контексте соответствующих операторов более простых типов.
Операторы ввода-вывода в стандартной библиотеке C++ определяются иначе. Общая схема выглядит так: сначала поток данных проходит предварительную обработку и готовится к вводу-выводу. Затем происходит собственно ввод-вывод и некоторая завершающая обработка. Используйте эту схему при определении собственных операторов ввода-вывода, тем самым будет обеспечена их логическая согласованность.
В классах basic_istream и basic_ostream определяется вспомогательный класс sentry. Конструктор этого класса выполняет предварительную обработку, а деструктор - соответствующие завершающие действия. Эти классы заменяют функции, использовавшиеся в предыдущих реализациях библиотеки IOStream (ipfx(), isfx(), opfx() и osfx()). Новая схема обеспечивает завершающую обработку даже в том случае, если ввод-вывод будет отменен из-за исключения.
Если оператор ввода-вывода использует функцию неформатированного ввода-вывода или напрямую работает с потоковым буфером, для него прежде всего необходимо сконструировать объект sentry. Последующая обработка будет зависеть от состояния этого объекта, по которому проверяется состояние потока данных. Для этой цели объект sentry обычно преобразуется к типу bool. Следовательно, операторы ввода-вывода в общем виде выглядят так:
sentry se(strm); // Косвенная организация // предварительной и завершающей обработки if (se) { . . . . // Собственно работа с потоком }
В аргументе конструктора класс sentry получает объект strm, для которого должны выполняться предварительный и завершающий этапы обработки.
Также необходимо дополнительно позаботиться о решении общих задач операторов ввода-вывода (синхронизации потоков данных, проверки нормального состояния потока данных, игнорировании пропусков и т. д.), а также дополнительных задач, зависящих от реализации. Например, в многопоточных средах, то есть в средах с несколькими параллельно функционирующими потоками выполнения (threads), такой задачей может быть блокировка доступа.
Для входных потоков данных при конструировании объекта sentry может передаваться необязательный логический признак, который указывает, что пропуски должны читаться даже при установленном флаге skipws:
sentry se(strm,true); // Не игнорировать пропуски при чтении
Следующий пример демонстрирует эту возможность для класса Row, представляющего строку в текстовом редакторе.
std::ostream& operator<< (std::ostream& strm, const Row& row) { // Организация предварительной и завершающей обработки std::ostream::sentry se(strm); if (se) { // Выполнение вывода strm.write(row.c_str(),row.len()); return strm; }
std::istream& operator>> (std::istream& strm, Row& row) { // Организация предварительной и завершающей обработки // - true: пропуски не игнорируются std::istream::sentry se(strm,true); if (se) { // Выполнение ввода char с; row.clear(); while (strm.get(c) && с != '\n') { row.append(c); } return strm; }
Очевидно, что эта архитектура может использоваться даже в том случае, если реализация основана не на вызове функций неформатированного ввода-вывода, а на применении операторов. Тем не менее использование членов классов basic_istream или basic_ostream для чтения илн записи символов внутри кода, защищенного объектами sentry, малоэффективно. По возможности следует использовать соответствующий объект baslc_streambuf.
На следующем шаге мы рассмотрим пользовательские форматные флаги.