C++17 STL Стандартная библиотека шаблонов — страница 71 из 119


8. Для объектов типа

ostream_iterator
нужен параметр шаблона, который указывает, переменные какого типа они могут выводить. Если мы напишем
ostream_iterator
, то в дальнейшем для вывода данных на экран будет применяться конструкция
ostream& operator(ostream&, const T&)
. Именно это свойство мы и реализовали до типа
bork
. На сей раз просто выводим целые числа, поэтому специализация выглядит как
ostream_iterator
. Для вывода информации на экран мы будем использовать поток
cout
, так что предоставим его в качестве параметра конструктора. Пройдем по вектору в цикле и присвоим каждый элемент
i
разыменованному итератору вывода. Именно так потоковые итераторы используются в алгоритмах STL.


  ostream_iterator oit {cout};

  for (int i : v) { *oit = i; }

  cout << '\n';


9. Полученный от итератора результат нам подходит, но он выводит числа без каких-либо разделителей. Если мы хотим добавить пробелы-разделители между всеми выведенными элементами, то можем предоставить собственную строку с пробелами в качестве второго параметра конструктора итератора выводного потока. Данное действие позволит вывести строку

"1, 2, 3, 4, 5, "
вместо строки
"12345"
. К сожалению, мы не можем указать отбросить пробел с запятой после последнего числа, поскольку итератор не знает, что достиг конца строки, до тех пор, пока это не случится.


  ostream_iterator oit_comma {cout, ", "};

  for (int i : v) { *oit_comma = i; }

  cout << '\n';


10. Присвоение элементов итератору потока вывода для вывода их на экран — один из корректных способов использования итератора, но его создавали не для этого. Идея заключается в том, чтобы применить их вместе с алгоритмами. Самым простым алгоритмом является

std::copy
. Можно предоставить начальный и конечный итераторы вектора в качестве входного диапазона данных, а также итератор вывода потока в качестве итератора вывода. Алгоритм выведет все числа, содержащиеся в векторе. Сделаем это для обоих итераторов вывода, а затем сравним полученный результат с результатом работы циклов, написанных нами ранее:


  copy(begin(v), end(v), oit);

  cout << '\n';

  copy(begin(v), end(v), oit_comma);

  cout << '\n';


11. Помните функцию

word_num
, которая соотносит числа и строки —
1
с
"one "
,
2
с
"two"
и т.д.? Можно использовать для вывода данных и ее. Нужен только оператор вывода потока, имеющий шаблон, специализированный для типа
string
, поскольку мы больше не выводим целые числа. Вместо алгоритма
std::copy
станем использовать алгоритм
std::transform
, поскольку он позволяет применять функцию преобразования для каждого элемента входного диапазона до того, как эти элементы будут скопированы в выходной диапазон.


  transform(begin(v), end(v),

            ostream_iterator<string>{cout, " "},

word_num);

  cout << '\n';


12. В последней строке выходных данных наконец используем структуру

bork
. Мы могли бы передать алгоритму
std::transform
функцию преобразования, но не стали. Вместо этого можно просто создать итератор потока вывода, который специализирован для типа
bork
в вызове
std::copy
. В результате экземпляры типа
bork
будут неявно создаваться на основе целых чисел, находящихся в диапазоне данных. Это даст интересные выходные данные:


  copy(begin(v), end(v),

       ostream_iterator{cout, "\n"});

}


13. Компиляция и запуск программы дадут следующий результат. Первые две строки полностью идентичны следующим двум строкам, как мы и ожидали.

Далее мы получили «аккуратные» строки, содержащие текстовое написание чисел, разделенное пробелами, а после этого — множество строк

bork!
, для них был использован разделитель
"\n"
вместо пробелов.


$ ./ostream_printing 12345

1, 2, 3, 4, 5,

12345

1, 2, 3, 4, 5,

one two three four five bork!

bork! bork!

bork! bork! bork!

bork! bork! bork! bork!

bork! bork! bork! bork! bork!


Как это работает

Мы увидели, что итератор

std::ostream_iterator
— всего лишь оболочка для функции вывода данных на экран, имеющая форму и синтаксис итератора. Инкрементирование этого итератора ни к чему не приводит. Его разыменование возвращает лишь прокси-объект, чей оператор присваивания перенаправляет аргумент в поток вывода.

Итераторы потока вывода, специализированные для типа

T
(например,
ostream_ iterator
), работают для всех типов, для которых предоставлена реализация
ostream& operator<<(ostream&, const T&)
.

Итератор

ostream_iterator
всегда пытается вызвать оператор
<<
того типа, для которого он был специализирован, с помощью своего параметра шаблона. Он попробует неявно преобразовать типы, если это разрешено. Когда мы итерируем по диапазону элементов типа
A
, но копируем данные элементы в экземпляры типа
output_iterator
, то код будет работать при условии, что тип
A
можно неявно преобразовать к типу
B
. Мы сделали именно это для структуры
bork
: экземпляр типа
bork
можно неявно получить из целочисленного значения. Как следствие, можно легко сгенерировать множество строк
"bork! "
на консоли пользователя.

Если неявное преобразование выполнить нельзя, можно провести его самостоятельно с помощью алгоритма

std::transform
, что мы и сделали в комбинации с функцией
word_num
.


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

Перенаправляем выходные данные в файл для конкретных разделов кода

Поток

std::cout
предоставляет удобный способ вывести на экран все, что мы хотим и когда хотим, поскольку его действительно просто использовать, легко расширять и получать к нему доступ глобально. Даже если мы желаем выводить особые сообщения, например сообщения об ошибках, которые нужно изолировать от обычных сообщений, то можем просто задействовать поток
std::cerr
. Он похож на
cout
, но выводит данные в стандартный канал ошибок, а не в стандартный канал для выходных данных.

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

Перенаправить выходные данные объектов потока можно. Далее мы рассмотрим, как это сделать очень простым и элегантным способом.


Как это делается

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


1. В этот раз нужны только заголовочные файлы для потоков ввода/вывода и файлового потока. Кроме того, мы объявим об использовании пространства имен

std