C++. Сборник рецептов.

10.3. Написание своих собственных манипуляторов потока.

Проблема.

Требуется иметь манипулятор потока, который делает что-нибудь такое, что не могут делать стандартные манипуляторы. Или вам нужен такой один манипулятор, который устанавливает несколько флагов потока, и вам не приходится вызывать несколько манипуляторов всякий раз, когда необходимо установить конкретный формат вывода.

Решение.

Чтобы создать манипулятор, который не имеет аргументов (типа left), напишите функцию, которая принимает в качестве параметра ios_base и устанавливает для него флаги потока. Если вам нужен манипулятор с аргументом, см. приводимое ниже обсуждение. Пример 10.4 показывает возможный вид манипулятора без аргументов.

Пример 10.4. Простой манипулятор потока.

#include <iostream>

#include <iomanip>

#include <string>

Using namespace std;

// вывести числа с плавающей точкой в обычном виде.

Inline ios_base& floatnormal(ios_base& io) {

 io.setf(0, ios_base::floatfield);

 return(io);

}

Int main() {

 ios_base::fmtflags flags = // Сохранить старые флаги.

  cout.flags();

 double pi = 22.0/7.0;

 cout << pi = " << scientific // Научный режим.

  << pi * 1000 << '\n';

 cout << "pi = " << floatnormal << pi << '\n';

 cout.flags(flags);

}

Обсуждение.

Существует два вида манипуляторов: с аргументами и без аргументов. Манипуляторы без аргументов пишутся просто. Вам требуется только написать функцию, которая принимает в качестве параметра поток, выполнить с ним какие-то действия (установить флаги или изменить установочные параметры) и возвратить его. Сложнее написать манипулятор, который имеет один или несколько аргументов, потому что потребуется создавать дополнительные классы и функции, которые работают «за кулисами». Поскольку манипуляторы без аргументов более простые, начнем с них.

Прочитав рецепт 10.1, вероятно, вы поняли, что существует три формата вывода чисел с плавающей точкой и только два манипулятора для выбора формата. Для используемого по умолчанию формата не предусмотрен манипулятор; вам придется соответствующим образом установить флаг для потока, чтобы вернуться к стандартному формату:

Myiostr.setf(0, ios_base::float field);

Но для удобства вы можете добавить свой собственный манипулятор, делающий то же самое. Именно это сделано в примере 10.4. Манипулятор floatnormal устанавливает соответствующий флаг потока для вывода чисел с плавающей точкой в стандартном формате.

Компилятор знает, что делать с вашей новой функцией, потому что в стандартной библиотеке определен следующий оператор для basic_ostream ( basic_ostream — имя шаблона класса, инстанцируемого в классах ostream и wostream).

Basic_ostream<charT, traits>& operator<<

 (basic_ostream<charT, traits>& (* pf)basic_ostream<charT, traits>&)).

Здесь pf — это указатель функции, которая принимает в аргументе ссылку на basic_ostream и возвращает ссылку на basic_ostream. Этот оператор просто обеспечивает вызов вашей функции, которая принимает в качестве аргумента текущий поток.

Манипуляторы с аргументами более сложные. Чтобы понять причину этого, рассмотрим работу манипулятора без аргументов. Пусть используется, например, следующий манипулятор.

Myostream << myManip << "foo";

Вы задаете его без скобок, поэтому его имя в действительности заменяется адресом функции вашего манипулятора. В действительности operator<< вызывает функцию манипулятора и передает ей поток, чтобы манипулятор мог выполнить свою работу.

Для сравнения представим, что у вас имеется манипулятор, который принимает числовой аргумент, так что в идеале вы могли бы его использовать следующим образом.

Myostream << myFancyManip(17) << "apple";

Как это будет работать? Если вы считаете, что myFancyManip является функцией, принимающей целочисленный аргумент, то возникает проблема: как передать поток в функцию без включения его в параметры и явного его использования? Вы могли бы написать так.

Myostream << myFancyManip(17, myostream) << "apple";

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

Решение состоит в том, чтобы заставить компилятор пойти окольным путем. Вместо того чтобы operator<< вызывал функцию вашего манипулятора для потока, вам надо просто создать временный объект, который возвращает нечто такое, что может использовать operator<<.

Во-первых, вам необходимо определить временный класс, который делал бы всю работу. Для простоты предположим, что вам требуется написать манипулятор с именем setWidth, который делает то же самое, что и setw. Временная структура, которую вам необходимо построить, будет выглядеть примерно так.

Class WidthSetter {

Public:

 WidthSetter(int n) : width_(n) {}.

 void operator()(ostream& os) const {os.width(width_);}.

Private:

 int width_;

};

Этот класс содержит простую функцию. Предусмотрите в ней целочисленный аргумент и, когда operator() вызывается с аргументом потока, установите ширину для потока в значение, в какое она была установлена при инициализации этого объекта. В результате мы получим WidthSetter, сконструированный одной функцией и используемый другой. Ваш манипулятор конструирует эту функцию, и это будет выглядеть следующим образом.

WidthSetter setWidth(int n) {

 return(WidthSetter(n)); // Возвращает инициализированный объект.

}

Эта функция всего лишь возвращает объект WidthSetter, инициализированный целым значением. Этот манипулятор вы будете использовать в строке операторов operator<< следующим образом.

Myostream << setWidth(20) << "banana";

Но этого недостаточно, потому что setWidth просто возвращает объект WidthSetter; operator<< не будет знать, что с ним делать. Вам придется перегрузить operator<<, чтобы он знал, как управлять объектом WidthSetter:

Ostream& operator<<(ostream& os, const WidthSetter& ws) {

 ws(os);     // Передать поток объекту ws.

 return(os); // для выполнения реальной работы.

}

Это решает проблему, но не в общем виде. Вам не захочется писать класс типа WidthSetter для каждого вашего манипулятора, принимающего аргумент (возможно, вы это и делаете, но были бы не против поступить по-другому), поэтому лучше использовать шаблоны и указатели функций для получения привлекательной, обобщенной инфраструктуры, на базе которой вы можете создавать любое количество манипуляторов. Пример 10.5 содержит класс ManipInfra и версию operator<<, использующую аргументы шаблона для работы с различными типами символов, которые может содержать поток, и с различными типами аргументов, которые могут быть использованы манипулятором потока.

Пример 10.5. Инфраструктура манипуляторов.

#include <iostream>

#include <string>

Using namespace std;

// ManipInfra - это небольшой промежуточный класс, который помогает.

// создавать специальные манипуляторы с аргументами. Вызывайте его.

// конструктор с предоставлением указателя функции и значения из основной.

// функции манипулятора.

// Указатель функции должен ссылаться на вспомогательную функцию, которая.

// делает реальную работу. См. примеры ниже.

Template<typename T, typename C>

Class ManipInfra {

Public:

 ManipInfra(basic_ostream<C>& (*pFun) (basic_ostream<C>&, T), T val) :

  manipFun_(pFun), val_(val) {}.

 void operator()(basic_ostream<C>& os) const {

  manipFun_(os, val_);

 }       // Вызовите функцию, задавая ее указатель и.

Private: // передавая ей поток и значение.

 T val_;

 basic_оstrеаm<С>& (*manipFun_) (basic_ostream<C>&, T);

};

Template<typename T, typename C>

Basic_ostream<C>& operator<<(basic_ostream<C>& os,

 const ManipInfra<T, C>& manip) {

 manip(os);

 return(os);

}

// Вспомогательная функция, которая вызывается в итоге в классе ManipInfra.

Ostream& setTheWidth(ostream& os, int n) {

 os.width(n);

 return(os);

}

// Собственно функция манипулятора. Именно она используется в клиентском.

// программном коде.

ManipInfra<int, char> setWidth(int n) {

 return(ManipInfra<int, char>(setTheWidth, n));

}

// Ещё одна вспомогательная функция, которая принимает аргумент типа char.

Ostream& setTheFillChar(ostream& os, char с) {

 os.fill(c);

 return(os);

}

ManipInfra<char, char> setFill(char c) {

 return(ManipInfra<char, char>(setTheFillChar, c));

}

Int main() {

 cout << setFill('-').

  << setWidth(10) << right << "Proust\n";

}

Если последовательность событий при работе этого класса все же остается неясной, я советую прогнать пример 10.5 через отладчик. Увидев его реальную работу, вы все поймете.