Язык программирования C++ для профессионалов

       

Манипуляторы


К ним относятся разнообразные операции, которые приходится применять сразу перед или сразу после операции ввода-вывода. Например:

cout << x; cout.flush(); cout << y;

cin.eatwhite(); cin >> x;

Если писать отдельные операторы как выше, то логическая связь между операторами неочевидна, а если утеряна логическая связь, программу труднее понять.

Идея манипуляторов позволяет такие операции как flush() или eatwhite() прямо вставлять в список операций ввода-вывода. Рассмотрим операцию flush(). Можно определить класс с операцией operator<<(), в котором вызывается flush():

class Flushtype { };

ostream& operator<<(ostream& os, Flushtype) { return flush(os); }

определить объект такого типа

Flushtype FLUSH;

и добиться выдачи буфера, включив FLUSH в список объектов, подлежащих выводу:

cout << x << FLUSH << y << FLUSH ;

Теперь установлена явная связь между операциями вывода и сбрасывания буфера. Однако, довольно быстро надоест определять класс и объект для каждой операции, которую мы хотим применить к поточной операции вывода. К счастью, можно поступить лучше. Рассмотрим такую функцию:

typedef ostream& (*Omanip) (ostream&);

ostream& operator<<(ostream& os, Omanip f) { return f(os); }



Здесь операция вывода использует параметры типа "указатель на функцию, имеющую аргумент ostream& и возвращающую ostream&". Отметив, что flush() есть функция типа "функция с аргументом ostream& и возвращающая ostream&", мы можем писать

cout << x << flush << y << flush;

получив вызов функции flush(). На самом деле в файле <iostream.h> функция flush() описана как

ostream& flush(ostream&);

а в классе есть операция operator<<, которая использует указатель на функцию, как указано выше:

class ostream : public virtual ios { // ... public: ostream& operator<<(ostream& ostream& (*)(ostream&)); // ... };

В приведенной ниже строке буфер выталкивается в поток cout дважды в подходящее время:


cout << x << flush << y << flush;

Похожие определения существуют и для класса istream:

istream& ws(istream& is ) { return is.eatwhite(); }

class istream : public virtual ios { // ... public: istream& operator>>(istream&, istream& (*) (istream&)); // ... };

поэтому в строке

cin >> ws >> x;

действительно обобщенные пробелы будут убраны до попытки чтения в x. Однако, поскольку по умолчанию для операции >> пробелы "съедаются" и так, данное применение ws() избыточно.

Находят применение и манипуляторы с параметрами. Например, может появиться желание с помощью

cout << setprecision(4) << angle;

напечатать значение вещественной переменной angle с точностью до четырех знаков после точки.

Для этого нужно уметь вызывать функцию, которая установит значение переменной, управляющей в потоке точностью вещественных. Это достигается, если определить setprecision(4) как объект, который можно "выводить" с помощью operator<<():

class Omanip_int { int i; ostream& (*f) (ostream&,int); public: Omanip_int(ostream& (*ff) (ostream&,int), int ii) : f(ff), i(ii) { } friend ostream& operator<<(ostream& os, Omanip& m) { return m.f(os,m.i); } };

Конструктор Omanip_int хранит свои аргументы в i и f, а с помощью operator<< вызывается f() с параметром i. Часто объекты таких классов называют объект-функция. Чтобы результат строки

cout << setprecision(4) << angle

был таким, как мы хотели, необходимо чтобы обращение setprecision(4) создавало безымянный объект класса Omanip_int, содержащий значение 4 и указатель на функцию, которая устанавливает в потоке ostream значение переменной, задающей точность вещественных:

ostream& _set_precision(ostream&,int);

Omanip_int setprecision(int i) { return Omanip_int(&_set_precision,i); }

Учитывая сделанные определения, operator<<() приведет к вызову precision(i).

Утомительно определять классы наподобие Omanip_int для всех типов аргументов, поэтому определим шаблон типа:



template<class T> class OMANIP { T i; ostream& (*f) (ostream&,T); public: OMANIP(ostream (*ff) (ostream&,T), T ii) : f(ff), i(ii) { }

friend ostream& operator<<(ostream& os, OMANIP& m) { return m.f(os,m.i) } };

С помощью OMANIP пример с установкой точности можно сократить так:

ostream& precision(ostream& os,int) { os.precision(i); return os; }

OMANIP<int> setprecision(int i) { return OMANIP<int>(&precision,i); }

В файле <iomanip.h> можно найти шаблон типа OMANIP, его двойник для istream - шаблон типа SMANIP, а SMANIP - двойник для ioss. Некоторые из стандартных манипуляторов, предлагаемых поточной библиотекой, описаны ниже. Отметим,что программист может определить новые необходимые ему манипуляторы, не затрагивая определений istream, ostream, OMANIP или SMANIP.

Идею манипуляторов предложил А. Кениг. Его вдохновили процедуры разметки (layout ) системы ввода-вывода Алгола68. Такая техника имеет много интересных приложений помимо ввода-вывода. Суть ее в том, что создается объект, который можно передавать куда угодно и который используется как функция. Передача объекта является более гибким решением, поскольку детали выполнения частично определяются создателем объекта, а частично тем, кто к нему обращается.

Содержание раздела