На этом шаге мы рассмотрим общие принципы работы манипуляторов.
Реализация манипуляторов основана на очень простом приеме, который не только упрощает управление потоками данных, но и наглядно демонстрирует мощь механизма перегрузки функций. Манипуляторы представляют собой обычные функции, передаваемые операторам ввода-вывода в аргументах. Оператор вызывает переданную функцию. Например, оператор вывода класса ostream перегружается примерно так:
ostream& ostream::operator << (ostream (*op)(ostream&)) { // Вызов функцию, передаваемой в параметре, с аргументом-потоком return (*op)(*this); }
Аргумент op представляет собой указатель на функцию. Точнее говоря, это функция, которая получает и возвращает поток данных ostream (предполагается, что возвращается тот же объект ostream, который был получен при вызове). Если второй операнд оператора << является такой функцией, то при ее вызове в аргументе передается первый операнд оператора <<.
На первый взгляд описание кажется очень сложным, но в действительности все относительно просто. Следующий пример поможет лучше разобраться в происходящем. Манипулятор (то есть функция) endl() для объекта ostream реализуется примерно так:
std::ostream& std::endl (std::ostream& strm) { // Запись признака конца строки strm.put('\n'); // Принудительный вывод выходного буфера strm.flush(); // Возвращение strm для организации цепочечных вызовов return strm; }
Манипулятор используется в выражениях вида
std::cout << std::endl;
Для потока данных cout вызывается оператор <<, которому во втором операнде передается функция endl(). Реализация оператора << преобразует этот вызов в вызов переданной функции, которой в качестве аргумента передается объект потока данных:
std::endl (std::cout)
Чтобы добиться эффекта "вывода" манипулятора, можно просто использовать это выражение. Более того, у функциональной записи есть свои преимущества - она не требует указания пространства имен:
endl(std::cout)
Это возможно благодаря тому, что функции ищутся в том пространстве имен, в котором определены их аргументы.
Поскольку потоковые классы оформлены в виде шаблонов, параметризованных по типу символов, настоящая реализация endl() выглядит примерно так:
tempiate<class charT, class traits> std::basic_ostream<charT,traits>& std::endl (std::basic_ostream<charT,traits>& strm) { strm.put(strm.widen('\n')); strm.flush(); return strm; }
Функция widen() преобразует символ новой строки к кодировке, используемой в потоке данных.
В стандартную библиотеку C++ также включены параметризованные манипуляторы (то есть манипуляторы, которым при вызове передаются аргументы). Принципы работы этих манипуляторов зависят от реализации; не существует стандартных правил определения пользовательских параметризованных манипуляторов.
Стандартные параметризованные манипуляторы определяются в заголовоч-ном файле <iomanip>. Если вы собираетесь использовать их, в программу необходимо включить соответствующий файл:
#include <iomanip>
Все стандартные параметризованные манипуляторы связаны с форматированием данных, поэтому они будут рассматриваться далее при описании средств форматирования.
На следующем шаге мы рассмотрим пользовательские манипуляторы.