Глава 7 Строки, классы потоков и регулярные выражения

В этой главе:

□ создание, конкатенация и преобразование строк;

□ удаление пробелов из начала и конца строк;

□ преимущества использования

std::string
без затрат на создание объектов
std::string
;

□ чтение значений из пользовательского ввода;

□ подсчет всех слов в файле;

□ форматирование ваших выходных данных с помощью манипуляторов потока ввода/вывода;

□ инициализация сложных объектов из файла вывода;

□ заполнение контейнеров с применением итераторов

std::istream
;

□ вывод любых данных на экран с помощью итераторов

std::ostream
;

□ перенаправление выходных данных в файл для конкретных разделов кода;

□ создание пользовательских строковых классов путем наследования

std::char_traits
;

□ токенизация входных данных с помощью библиотеки для работы с регулярными выражениями;

□ удобный и красивый динамический вывод чисел на экран в зависимости от контекста;

□ перехват читабельных исключений для ошибок потока

std::iostream
.

Введение

Данная глава посвящена обработке строк, анализу и выводу на экран произвольных данных. Для этих задач STL предоставляет потоковую библиотеку для работы с вводом-выводом. Библиотека состоит из следующих классов, показанных в серых прямоугольниках (рис. 7.1).

Стрелки показывают схему наследования классов. Рисунок на первый взгляд может показаться непонятным, но в рамках главы мы рассмотрим все классы и познакомимся с ними по очереди. Если мы попробуем обратиться к документации С++ в STL, то не найдем упомянутые классы по конкретно этим именам. Причина такова: названия, приведенные на рис. 7.1, мы видим только как программисты приложений. Но они, по сути, являются просто ключевыми словами для классов с префиксом имени класса

basic_
(например, в документации STL проще найти класс
basic_istream
, в отличие от
istream
). Классы для работы с потоками, которые начинаются с префикса
basic_*
, являются шаблонными, они могут быть специализированы для разных типов символов. Классы, показанные на рис. 7.1, специализированы для значений типа
char
. В книге мы будем применять именно эти специализации. Если мы воспользуемся префиксом
w
для упомянутых имен класса, то получим названия
wistream
,
wostream
и т.д. — они являются специализациями для типа
wchar_t
вместо
char
.

В верхней части рис. 7.1 мы видим класс

std::ios_base
. Мы практически никогда не будем использовать непосредственно его; он приведен для полноты картины, поскольку другие классы наследуют от него. Следующая специализация — это
std::ios
, она воплощает идею объекта, сопровождающего поток данных, который может находиться в исправном состоянии, в состоянии, когда закончились данные (empty of data, EOF) или в другом ошибочном состоянии.

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

std::istream
и
std::ostream
. Префиксы
"i"
и
"o"
расшифровываются как
input
и
output
(«ввод» и «вывод»). Мы уже видели такие префиксы на раннем этапе программирования на С++ в простейших примерах в объектах
std::cout
и
std::cin
(а также
std::cerr
). Экземпляры этих классов всегда доступны глобально. Мы выполняем вывод данных с помощью
ostream
, а ввод — с использованием
input
.

Класс, который наследует от классов

istream
и
ostream
, называется
iostream
. С его помощью можно выполнять как ввод, так и вывод данных. Зная, как использовать все классы из трио
istream
,
ostream
и
iostream
, вы сможете незамедлительно применить следующие классы:

□ классы

ifstream
,
ofstream
и
fstream
наследуют от классов
istream
,
ostream
и
iostream
соответственно, но задействуют их возможности, чтобы перенаправить ввод/вывод в файлы или из них из файловой системы компьютера;

□ классы

istringstream
,
ostringstream
и
iostringstream
работают по схожему принципу. Они помогают создавать строки в памяти, а затем помещают туда данные или считывают их.

Создание, конкатенация и преобразование строк

Даже те, кто довольно давно пользовался языком C++ , знают о классе

std::string
. Хотя в языке C обработка строк довольно утомительна, особенно при анализе, конкатенации и копировании и т.д., класс
std::string
— это реальный шаг вперед к простоте и безопасности.

Благодаря выходу C++11 теперь даже не нужно копировать строки, когда мы хотим передать право собственности какой-то другой функции или структуре данных, поскольку можем перемещать их. Таким образом, в подобных случаях не возникает больших издержек.

По мере выхода новых стандартов класс

std::string
получил несколько новых свойств. Совершенно новой особенностью С++17 является
std::string_view
. Мы немного поработаем с ней (но впереди будет и другой пример, в котором более подробно рассматривается
std::string_view
), чтобы понять, как взаимодействовать с подобным инструментарием в эпоху С++17.


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

В этом примере мы создадим строки и строковые представления, а затем выполним простые операции конкатенации и преобразования.


1. Как и обычно, сначала включим заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 

#include 

#include 


using namespace std;


2. Сначала создадим объекты строк. Самым очевидным способом является инстанцирование объекта класса

string
. Мы контролируем его содержимое, передавая конструктору строку в стиле C (которая после компиляции будет встроена в бинарный файл как статический массив, содержащий символы). Конструктор скопирует ее и сделает содержимым объекта строки
a
. Помимо этого, вместо инициализации строки с помощью строки в стиле C можно применить оператор строкового литерала
""s
. Он создает объект строк динамически. Мы используем его для создания объекта
b
, что позволит применять автоматическое выведение типа.


int main()

{

  string a { "a" };

  auto b ( "b"s );


3. Строки, которые мы только что создали, копируют их входные данные из аргумента конструктора в собственный буфер. Чтобы не копировать строку, а просто сослаться на нее, можно воспользоваться объектами типа

string_ view
. Этот класс также имеет оператор литерала, он вызывается с помощью конструкции
""sv
:


  string_view c { "c" };

 auto d ( "d"sv );


4. Теперь поработаем с нашими строками и строковыми представлениями. Для обоих типов существует перегруженная версия оператора

<<
для класса
std::ostream
, поэтому их удобно выводить на экран:


  cout << a << ", " << b << '\n';

 cout << c << ", " << d << '\n';


5. В классе

string
имеется перегруженная версия оператора
+
, поэтому можно сложить две строки и получить в качестве результата их конкатенацию. Таким образом, выражение
"a" + "b"
даст результат
"ab"
. Конкатенация строк
a
и
b
с помощью данного способа выполняется довольно легко. При необходимости сложить
a
и
c
могут возникнуть некоторые трудности, поскольку
c
— не строка, а экземпляр класса
string_view
. Сначала нужно получить строку из
c
, это делается путем создания новой строки из
c
и сложения ее с
a
. Кто-то может задаться вопросом: «Погодите, зачем копировать с в промежуточную строку только для того, чтобы сложить ее с
а
? Можно ли этого избежать, использовав конструкцию
c.data()
?» Идея хороша, но имеет недостаток: экземпляры класса
string_view
не обязаны содержать строки, завершающиеся нулем. Данная проблема может привести к переполнению буфера.


  cout << a + b << '\n';

  cout << a + string{c} << '\n';


6. Создадим новую строку, содержащую все введенные нами строки и строковые представления. С помощью

std::ostringstream
можно поместить любую переменную в объект потока, который ведет себя точно так же, как и
std::cout
, но не выводит данные на консоль. Вместо этого он выводит данные в строковый буфер. После того, как мы поместим все переменные в поток, разделив их пробелами с помощью оператора
<<
, можем создать и вывести на экран новый объект строки, задействовав конструкцию
o.str()
.


  ostringstream o;

  o << a << " " << b << " " << c << " " << d;

  auto concatenated (o.str());

  cout << concatenated << '\n';


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

toupper
, которая соотносит символы в нижнем регистре и символы в верхнем, оставляя другие символы неизменными, уже доступна, и ее можно объединить с функцией
std::transform
, поскольку строка, по сути, представляет собой итерабельный контейнер, содержащий элементы типа
char
.


  transform(begin(concatenated), end(concatenated),

       begin(concatenated), ::toupper);

  cout << concatenated << '\n';

}


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


$ ./creating_strings

a, b

c, d

ab

ac

a b c d

A B C D


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

Очевидно, строки можно складывать с помощью оператора

+
прямо как числа. Математика здесь не используется, но в итоге мы получаем сконкатенированные строки. Чтобы иметь возможность работать с объектами класса
string_view
, сначала нужно выполнить их преобразование к типу
std::string
.

Однако очень важно отметить: при объединении в коде строк и строковых представлений мы никогда не должны предполагать, что хранящаяся в экземпляре класса

string_view
строка завершается нулевым символом! Именно поэтому мы используем конструкцию
"abc"s + string{some_string_view}
вместо конструкции
"abc"s + some_string_view.data()
. Кроме того, класс
std::string
предоставляет функцию-член
append
, которая может работать с экземплярами класса
string_view
, но изменяет строку вместо того, чтобы вернуть новую строку, к которой прикреплено содержимое строкового представления.


Класс

std::string_view
очень полезен, но будьте осторожны при смешивании его со строками и строковыми функциями. Нельзя рассчитывать на то, что эти строки будут оканчиваться нулями, поэтому весь порядок быстро нарушается в стандартном строковом окружении. К счастью, зачастую предусмотрены подходящие перегруженные версии функций, которые могут обрабатывать подобные ситуации.


Сложную конкатенацию строк с форматированием и т.д. не следует выполнять шаг за шагом для экземпляров строк. Классы

std::stringstream
,
std::ostringstream
и
std::istringstream
подходят для этого гораздо лучше, так как позволяют более качественно управлять памятью при объединении строк и предоставляют все возможности по форматированию, характерные для потоков. Для данного раздела мы выбрали класс
std::ostringstream
, поскольку собираемся создать строку вместо того, чтобы ее анализировать. Экземпляр класса
std::istringstream
можно создать на основе существующей строки, которая при необходимости преобразуется в переменные других типов. Если мы хотим объединить обе возможности, то нам подойдет класс
std::stringstream
.

Удаляем пробелы из начала и конца строк

Полученные из пользовательского ввода строки зачастую содержат лишние пробелы. В одном из предыдущих примеров мы удаляли повторяющиеся пробелы между словами.

Взглянем на пробелы, расположенные вокруг строк, и удалим их. Класс

std::string
содержит удобные вспомогательные функции, которые нам помогут.


После рассмотрения данного примера, в котором показано, как работать с простыми строковыми объектами, взгляните также и на следующий пример. Там вы увидите, как избежать ненужного копирования или изменения данных при работе с классом

std::string_view
.


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

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


1. Как и обычно, сначала идут заголовочные файлы и директива

using
:


#include 

#include 

#include 

#include 


using namespace std;


2. Наша функция будет принимать константную ссылку на существующую строку. А возвращать станет новую строку, из начала и конца которой удалены лишние пробелы:


string trim_whitespace_surrounding(const string &s)

{


3. Класс

std::string
предоставляет две удобные функции, которые будут очень полезны. Первая функция — это
string::find_first_not_of
, она принимает строку, содержащую все символы, которые мы хотим опустить. В нашем случае таковыми являются символы пробела
' '
, табуляции
'\t'
и перехода на новую строку
'\n'
. Функция возвращает позицию первого символа, не совпадающего с переданными. При наличии в строке только пробелов она вернет значение
string::npos
. Это значит следующее: если мы удалим все пробелы, то останется только пустая строка. Так что в подобных случаях просто возвращайте пустую строку:


  const char whitespace[] {" \t\n"};

  const size_t first (s.find_first_not_of(whitespace));

 if (string::npos == first) { return {}; }


4. Теперь мы знаем, где должна начинаться новая строка, но пока неизвестно, где она заканчивается. Поэтому воспользуемся другой полезной функцией строки:

string::find_last_not_of
. Она вернет позицию последнего символа, не являющегося пробелом.


  const size_t last (s.find_last_not_of(whitespace));


5. С помощью функции

string::substr
мы теперь можем вернуть часть строки, окруженной пробелами, в которой их не будет. Эта функция принимает два параметра: позицию от начала строки и количество символов после данной позиции.


  return s.substr(first, (last - first + 1));

}


6. На этом все. Напишем функцию

main
, в которой создадим строку, окружающую предложение всеми видами пробелов, чтобы обрезать ее:


int main()

{

  string s {" \t\n string surrounded by ugly"

       " whitespace \t\n "};


7. Выведем на экран необрезанную и обрезанную версии строки. Окружив строку скобками, можно показать все пробелы, которые находились в ней до обрезки.


  cout << "{" << s << "}\n";

 cout << "{"

    << trim_whitespace_surrounding(s)

    << "}\n";

}


8. Компиляция и запуск программы дадут ожидаемый результат:


$ ./trim_whitespace

{

  string surrounded by ugly whitespace

  }

{string surrounded by ugly whitespace}


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

В этом разделе мы применили функции

string::find_first_not_of
и
string::find_ last_not_of
. Обе принимают строку, созданную в стиле C, в виде списка символов, которые нужно проигнорировать при поиске другого символа. Если у нас есть экземпляр строки, содержащий строку
"foo bar"
, и мы вызовем для него функцию
find_first_not_of("bfo ")
, то она вернет значение
5
, поскольку символ
'a'
— первый символ, который не входит в строку
"bfo"
. Порядок символов в строке-аргументе неважен.

Существуют подобные функции с инвертированной логикой, однако мы не использовали их в этом примере:


string::find_first_of and string::find_last_of


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

string::npos
.

На основе позиций символов, полученных из этих функций, мы в рамках вспомогательной функции создаем подстроку, не содержащую пробелов в начале и конце, с помощью функции

string::substring
. Она принимает относительное смещение и длину строки, а затем возвращает новый экземпляр строки, для которого выделен собственный фрагмент памяти, содержащий только эту подстроку. Например, вызов
string{"abcdef"}.substr(2, 2)
вернет новую строку
"cd"
.

Преимущества использования std::string без затрат на создание объектов std::string

Класс

std::string
очень полезен, поскольку значительно упрощает работу со строками. Его недостаток заключается в том, что при необходимости передать подстроку нужно передавать указатель и переменную, содержащую длину подстроки, два итератора или копию подстроки. Мы делали это в предыдущем примере, когда удаляли лишние пробелы из строки, вернув копию подстроки, которая не содержит их.

Если мы хотим передать строку или подстроку в библиотеку, которая не предоставляет поддержку класса

std::string
, то можем передать только необработанный указатель на строку, что несколько разочаровывает, поскольку этот способ использовался еще во времена С. Как и в случае с выделением подстроки, необработанный указатель не несет информации о длине строки. Таким образом, кто-то должен будет реализовать связку указателя и длины строки.

Говоря упрощенно, такой конструкцией как раз и является класс

std::string_view
. Он доступен, начиная с версии C++17, и предоставляет способ объединения указателя на некую строку и ее размера. Он воплощает идею наличия ссылочного типа для массивов данных.

Представим, что разрабатываем функции, которые ранее в качестве параметров принимали объекты типа

std::string
, но при этом не изменяли их так, чтобы экземплярам класса string потребовалось повторно выделять память, содержащую реальные данные. Мы могли бы использовать тип
std::string_view
для повышения совместимости с библиотеками, которые не знают об STL. Можно позволить другим библиотекам предоставлять
string_view
для строк, содержащих полезные данные, скрытые за сложной реализацией типа
string
, а затем применять их в нашем коде, совместимом с STL. Таким образом, класс
string_view
ведет себя как минималистичный и полезный интерфейс, пригодный для использования многими библиотеками.

Еще одной приятной особенностью является тот факт, что класс

string_view
может применяться как ссылка на подстроки больших объектов класса
string
. Существует много возможностей грамотно использовать эту особенность. В данном разделе мы поработаем с классом
string_view
, чтобы получить представление о его преимуществах и недостатках. Кроме того, увидим, как можно скрыть пробелы в начале и конце строки путем адаптации строковых представлений, а не изменения или копирования самой строки. Этот метод позволяет избежать ненужного копирования или изменения данных.


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

В этом примере мы реализуем функцию, которая работает с особенностями класса

string_view
, а затем увидим, сколько разных типов можем ей передать.


1. Сначала указываем заголовочные файлы и директивы

using
:


#include 

#include 


using namespace std;


2. Реализуем функцию, которая принимает в качестве единственного аргумента объект типа

string_view
:


void print(string_view v)

{


3. Прежде чем сделать что-то с входной строкой, удалим все пробелы из ее начала и конца. Мы будем изменять не строку, а ее представление, сузив его до значащей части. Функция

find_first_not_of
найдет первый символ строки, который не является пробелом (
' '
), символом табуляции (
'\t'
) или символом перехода на новую строку (
'\n'
). С помощью функции
remove_prefix
мы переместим внутренний указатель класса
string_view
на первый символ, не являющийся пробелом. В том случае, если строка содержит только пробелы, функция
find_ first_not_of
вернет значение
npos
, которое равно
size_type(-1)
. Поскольку
size_type
— беззнаковая переменная, мы получим очень большое число. Поэтому выберем меньшее число из полученных: words_begin или размер строкового представления:


  const auto words_begin (v.find_first_not_of(" \t\n"));

  v.remove_prefix(min(words_begin, v.size()));


4. То же самое сделаем с пробелами в конце строки. Функция

remove_suffix
уменьшает переменную, показывающую размер строкового представления:


  const auto words_end (v.find_last_not_of(" \t\n"));

 if (words_end != string_view::npos) {

   v.remove_suffix(v.size() - words_end - 1);

  }


5. Теперь можно вывести на экран строковое представление и его длину:


  cout << "length: " << v.length()

    << " [" << v << "]\n";

}


6. В функции

main
воспользуемся новой функцией
print
, передав ей разные типы аргументов. Сначала передадим ей во время выполнения строку
char*
из указателя
argv
. Во время выполнения программы он будет содержать имя нашего исполняемого файла. Затем передадим ей пустой объект
string_view
. Далее передадим ей символьную строку, созданную в стиле С, а также строку, образованную с помощью литерала
""sv
, который динамически создаст объект типа
string_view
. И наконец, передадим ей объект класса
std::string
. Положительный момент заключается в следующем: ни один из данных аргументов не изменяется и не копируется, чтобы вызвать функцию
print
. Не происходит выделения памяти в куче. Для большого количества строк и/или для длинных строк это очень эффективно.


int main(int argc, char *argv[])

{

  print(argv[0]);

  print({});

  print("a const char * array");

 print("an std::string_view literal"sv);

 print("an std::string instance"s);


7. Мы не протестировали функцию удаления пробелов. Передадим ей строку, которая содержит множество пробелов в начале и в конце:


  print(" \t\n foobar \n \t ");


8. Еще одна приятная особенность класса

string_view
: он позволяет создавать строки, не завершающиеся нулевым символом. Если мы введем строку, например
"abc"
, которая не будет заканчиваться нулем, то функция
print
сможет безопасно ее обработать, поскольку объект класса
string_view
также содержит размер строки, на которую указывает:


  char cstr[] {'a', 'b', 'c'};

  print(string_view(cstr, sizeof(cstr)));

}


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

abc
, не имеющая завершающего нулевого символа, корректно выведена на экран, не вызвав переполнения буфера:


$ ./string_view

length: 17 [./string_view]

length: 0 []

length: 20 [a const char * array]

length: 27 [an std::string_view literal]

length: 23 [an std::string instance]

length: 6 [foobar]

length: 3 [abc]


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

Мы только что увидели следующее: можно вызвать функцию, принимающую аргумент типа

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

Интересно отметить, что в вызове

print(argv[0])
строковое представление автоматически определило длину строки, поскольку данная строка по соглашению завершается нулевым символом. С другой стороны, никто не может предполагать, что можно определить длину поля данных объекта типа
string_view
путем подсчета символов до тех пор, пока не встретится нулевой символ. Поэтому нужно всегда соблюдать осторожность при работе с указателями на данные строкового представления с помощью
string_view::data()
. Обычные строковые функции предполагают, что строка будет завершаться нулевым символом, и поэтому использование необработанных указателей способно привести к переполнению буфера. Всегда лучше применять интерфейсы, которые ожидают передачи строкового представления.

За исключением этой особенности, у нас имеется роскошный интерфейс, с которым мы уже знакомы благодаря классу

std::string
.


Задействуйте класс

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

Считываем значения из пользовательского ввода

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

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

std::cin
— по сути, это объект потока ввода, как и экземпляры классов
ifstream
и
istringstream
.


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

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


1. На сей раз нам понадобится только

iostream
. Так что включим этот единственный заголовочный файл и объявим об использовании пространства имен
std
по умолчанию:


#include 


using namespace std;


2. Пригласим пользователя ввести два числа. Поместим их в переменные типов

int
и
double
. Пользователь может разделить их пробелами. Например,
1 2.3
— это корректные входные данные.


int main()

{

  cout << "Please Enter two numbers:\n> ";

 int x;

  double y;


3. Анализ и проверка ошибок выполняются одновременно в условной части блока

if
. Выведем числа на экран только в том случае, если их можно проанализировать и они имеют смысл:


  if (cin >> x >> y) {

   cout << "You entered: " << x

     << " and " << y << '\n';


4. Если по какой-то причине анализ завершился ошибкой, то мы скажем пользователю об этом. Объект потока

cin
теперь находится в состоянии ошибки и не будет давать другие данные до тех пор, пока мы не избавимся от этого состояния. Чтобы иметь возможность проанализировать новые входные данные после ошибки, вызываем метод
cin.clear()
и отбрасываем все входные данные, полученные ранее. Отбрасывание выполняется с помощью
cin.ignore
, где мы указываем, что нужно отбросить максимальное количество символов до тех пор, пока не встретим символ новой строки (который тоже будет отброшен). После этого символа снова начинаются интересные входные данные:


  } else {

   cout << "Oh no, that did not go well!\n";

   cin.clear();

   cin.ignore(

    std::numeric_limits::max(),

    '\n');

  }


5. Теперь запросим еще какие-нибудь входные данные. Мы позволим пользователю вводить имена. Они могут состоять из нескольких слов, разделенных пробелами, поэтому символ пробела не подходит в качестве разделителя. Так что воспользуемся

std::getline
, принимающим объектом потока наподобие
cin
, ссылкой на строку, в которую будут скопированы входные данные, и символом-разделителем (таковым выступит запятая
','
). Задействуя вместо
cin
конструкцию
cin >> ws
в качестве параметра потока для
getline
, можно дать
cin
команду отбросить пробелы, стоящие перед именами. На каждом шаге цикла выводим текущее имя, но если оно пустое, то завершим цикл:


  cout << "now please enter some "

      "comma-separated names:\n> ";

  for (string s; getline(cin >> ws, s, ',');) {

   if (s.empty()) { break; }

   cout << "name: \"" << s << "\"\n";

  }

}


6. Компиляция и запуск программы дадут следующий результат (для него предполагается введение только корректных значений). Мы ввели числа

"1 2"
, они были преобразованы корректно, а затем ввели некоторые имена, также корректно выведенные на экран. Пустое имя, полученное в виде двух запятых, расположенных рядом друг с другом, завершает цикл:


$ ./strings_from_user_input Please Enter two numbers:

> 1 2

You entered: 1 and 2

now please enter some comma-separated names:

> john doe, ellen ripley, alice, chuck norris,,

name: "john doe"

name: "ellen ripley"

name: "alice"

name: "chuck norris"


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

cin.clear()
и
cin.ignore(...)
, чтобы увидеть, как это пересекается с кодом чтения имен:


$ ./strings_from_user_input

Please Enter two numbers:

> a b

Oh no, that did not go well!

now please enter some comma-separated names:

> bud spencer, terence hill,,

name: "bud spencer"

name: "terence hill"


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

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

Результатом выполнения выражения

cin >> x
является ссылка на
cin
. Таким образом можно создавать конструкции наподобие
cin >> x >> y >> z >> ....
В то же время можно преобразовать данные к булевым значениям, используя их в булевых контекстах, таких как условие
if
. Булево значение говорит нам, была ли корректной последняя операция чтения. Вот почему можно создавать конструкции, аналогичные выражению
if (cin >> x >> y){...}
.

Если мы, например, попробуем считать целое число, но во входных данных следующим является токен

"foobar"
, то преобразование этого значения к типу
int
выполнить нельзя и объект потока входит в ошибочное состояние. Это критически важно только для попытки преобразования, но не для всей программы. Можно совершенно спокойно сбросить ее и попробовать сделать что-то еще. В нашем примере мы попробовали считать список имен после потенциально ошибочной попытки считать два числа. При неудачной попытке считывания числа мы используем функцию
cin.clear()
с целью вернуть
cin
в рабочее состояние. Но после этого его внутренний курсор все еще находится в той позиции, где лежат данные, которые мы ввели вместо чисел. Чтобы отбросить старые входные данные и очистить канал для ввода имен, мы применили очень длинное выражение,
cin.ignore(std::numeric_limits::max(), '\n');
. Необходимо удалять все, что находилось в буфере, поскольку он нужен абсолютно пустым, когда мы запросим у пользователя список имен.

Следующий цикл также может показаться странным на первый взгляд:


for (string s; getline(cin >> ws, s, ',');) { ... }


В условной части цикла мы использовали функцию

getline
. Она принимает объект потока ввода, ссылку на строку в качестве выходного параметра, а также символ-разделитель. По умолчанию таковым является символ перехода на новую строку. Здесь мы указали, что в роли этого символа выступает запятая (
,
), чтобы все имена из списка наподобие
"john, carl, frank"
считывались отдельно.

Пока все идет по плану. Но что означает предоставление функции

cin >> ws
в качестве объекта потока? Это заставляет
cin
отбросить все пробелы, которые стоят перед следующим символом, не являющимся пробелом, а также в конце строки. Если бы мы не применили
ws
, то из строки
"john, carl, frank"
получили бы подстроки
"john"
,
" carl"
и
" frank"
. Заметили лишние пробелы для имен
carl
и
frank
? Они пропадают, поскольку мы использовали
ws
.

Подсчитываем все слова в файле

Предположим, мы считали текстовый файл и хотим определить количество слов в тексте. Словом, мы называем диапазон символов, расположенный между пробелами. Как же решить эту задачу?

Например, можно подсчитать количество пробелов, поскольку между словами должны быть пробелы. В предложении

"John has a funny little dog."
пять символов пробела, поэтому можно сказать, что оно состоит из шести слов.

Но как быть с предложением, содержащим разные виды пробелов, например:

"John has \t a\nfunny little dog."
? В нем содержится слишком много ненужных пробелов и не только. Из других примеров данной книги вы уже знаете, как удалить эти лишние пробелы. Так что сначала можно было бы предварительно обработать строку, преобразовав ее в обычное предложение, а затем применить стратегию подсчета пробелов. Это выполнимо, но существует гораздо более простой способ. Почему бы не воспользоваться теми возможностями, которые предоставляет STL?

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


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

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


1. Включим все необходимые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 

#include 

#include 


using namespace std;


2. Функция

wordcount
принимает поток ввода, например
cin
. Она создает итератор
std::input_iterator
, который токенизирует строки потока, а затем передает их в
std::distance
. Параметр
distance
принимает в качестве аргументов два итератора и пытается определить, сколько именно операций инкремента нужно выполнить, чтобы переместиться с одной позиции итератора в другую. Для итераторов с произвольным доступом это делается легко, поскольку они реализуют операцию математической разности (
operator-
). Такие итераторы можно вычитать друг из друга, как и другие указатели. Итератор
istream_iterator
, однако, является однонаправленным, и его нужно сдвигать вперед до тех пор, пока он не станет равен итератору
end
. В конечном счете количество шагов будет равно количеству слов.


template 

size_t wordcount(T &is)

{

  return distance(istream_iterator{is}, {});

}


3. В нашей функции

main
мы позволяем пользователю выбрать, откуда придет поток ввода — из
std::cin
или из файла:


int main(int argc, char **argv)

{

  size_t wc;


4. Если пользователь запустит программу в оболочке и укажет имя файла (например,

$ ./count_all_words some_textfile.txt
), то мы сможем получить его из параметра командной строки
argv
и открыть, чтобы передать новый поток ввода в функцию
wordcount
:


  if (argc == 2) {

    ifstream ifs {argv[1]};

   wc = wordcount(ifs);


5. В случае запуска пользователем программы без параметров мы предполагаем, что входные данные появятся из стандартного потока ввода:


  } else {

   wc = wordcount(cin);

  }


6. На этом все, просто выведем количество слов, сохраненное в переменной

wc
:


  cout << "There are " << wc << " words\n";

};


7. Скомпилируем и запустим программу. Сначала передадим данные из стандартного потока ввода. Можно либо выполнить вызов echo, передав туда несколько слов, либо запустить программу и ввести несколько слов с клавиатуры. Во втором случае можно прервать ввод нажатием комбинации клавиш Ctrl+D. Так выглядит вызов

echo
:


$ echo "foo bar baz" | ./count_all_words

There are 3 words


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


$ ./count_all_words count_all_words.cpp

There are 61 words


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

Здесь особо нечего добавить, большую часть программы мы уже объяснили при реализации, поскольку сама программа очень короткая. Единственный факт, который можно немного пояснить:

std::cin
и
std::ifstream
взаимозаменяемы.
cin
имеет тип
std::istream
, а
std::ifstream
наследует от
std::istream
. Взгляните на диаграмму наследования, приведенную в начале этой главы (см. рис. 7.1). Таким образом, они полностью взаимозаменяемы, даже во время работы программы.


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

Форматируем ваши выходные данные с помощью манипуляторов потока ввода-вывода

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

"0x"
нужен, а в других — нет.

При выводе чисел с плавающей точкой существует множество моментов, на которые можно повлиять. Нужно ли всегда выводить их с одинаковой точностью? Надо ли выводить их вообще? Или, может быть, требуется научное представление?

Помимо представления и системы счисления нужно также представить пользователю выходные данные в «аккуратном» виде. Некоторые выходные данные можно поместить, например, в таблицы, чтобы сделать их максимально читабельными.

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


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

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


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 


using namespace std;


2. Далее определим вспомогательную функцию, выводящую на экран целое число в разных стилях. Она принимает ширину отступа и символ-заполнитель, которым по умолчанию является

' '
:


void print_aligned_demo(int val,

             size_t width,

             char fill_char = ' ')

{


3. С помощью

setw
можно задать минимальное количество символов, которое нужно вывести. Например, при вводе числа
123
с шириной
6
получим
" 123"
или
" 123"
. Можно управлять тем, с какой стороны будут вставлены символы-заполнители, используя
std::left
,
std::right
и
std::internal
. При выводе чисел в десятичной форме манипулятор
internal
выглядит похожим на
right
. Но если мы выведем значение
0x1
, например с шириной
6
и манипулятором
internal
, то получим "
0x 6"
. Манипулятор
setfill
определяет, какой именно символ будет применен для заполнения. Мы попробуем разные стили.


  cout << "================\n"; 

  cout << setfill(fill_char); 

  cout << left << setw(width) << val << '\n'; 

 cout << right << setw(width) << val << '\n'; 

 cout << internal << setw(width) << val << '\n'; 

}


4. В функции

main
начинаем использовать реализованную нами функцию. Сначала выведем на экран значение
12345
с шириной
15
. Сделаем это дважды, но во второй раз применим заполнитель
'_'
.


int main()

{

  print_aligned_demo(123456, 15);

  print_aligned_demo(123456, 15, '_');


5. Затем выведем значение

0x123abc
с такой же шириной. Однако прежде, чем это сделать, применим
std::hex
и
std::showbase
с целью указать объекту потока вывода
cout
, что он должен выводить числа в шестнадцатеричном формате и добавлять к ним префикс
"0x"
, поскольку тогда их нельзя будет интерпретировать по-другому:


  cout << hex << showbase;

 print_aligned_demo(0x123abc, 15);


6. Сделаем то же самое и для

oct
, что укажет
cout
использовать восьмеричную систему счисления при выводе чисел.
showbase
все еще активен, поэтому
0
будет добавлен к каждому выводимому числу:


  cout << oct;

 print_aligned_demo(0123456, 15);


7. В случае использования

hex
и
uppercase
мы увидим, что символ
'x'
в конструкции
"0x"
будет выведен в верхнем регистре. Сочетание
'abc'
в конструкции
'0x123abc'
также будет в верхнем регистре:


  cout << "A hex number with upper case letters: "

    << hex << uppercase << 0x123abc << '\n';


8. Если мы хотим снова вывести число

100
в десятичной системе счисления, то нужно запомнить, что мы переключили поток в режим
hex
. С помощью
dec
вернем его в обычное состояние:


  cout << "A number: " << 100 << '\n';

  cout << dec;

  cout << "Oops. now in decimal again: " << 100 << '\n';


9. Мы также можем сконфигурировать формат вывода булевых значений. По умолчанию значение

true
выводится как
1
, а
false
— как
0
. С помощью
boolalpha
можно задать ему текстовое представление:


  cout << "true/false values: "

    << true << ", " << false << '\n';

 cout << boolalpha

    << "true/false values: "

    << true << ", " << false << '\n';


10. Взглянем на переменные с плавающей точкой типов

float
и
double
. Если нужно вывести число наподобие
12.3
, то увидим
12.3
. При наличии такого числа, как
12.0
, поток вывода отбросит десятичную точку. Это можно изменить, использовав
showpoint
. С его помощью десятичная точка будет отображаться всегда:


  cout << "doubles: "

     << 12.3 << ", "

    << 12.0 << ", "

    << showpoint << 12.0 << '\n';


11. Представление чисел с плавающей точкой может иметь модификаторы

scientific
или
fixed
. Первый означает следующее: число нормализовано к такому виду, что видна только первая цифра после десятичной точки, а затем выводится экспонента, на которую нужно умножить число, чтобы получить его реальный размер. Например, значение
300.0
будет выглядеть как
"3.0E2"
, поскольку
300
равно
3.0 * 10^2
. Модификтор
fixed
позволяет вернуться к представлению с десятичной точкой:


  cout << "scientific double: " << scientific

    << 123000000000.123 << '\n';

  cout << "fixed double: " << fixed

    << 123000000000.123 << '\n';


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


  cout << "Very precise double: "

    << setprecision(10) << 0.0000000001 << '\n';

 cout << "Less precise double: "

    << setprecision(1) << 0.0000000001 << '\n';

}


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

print
с помощью модификаторов
setw
и
left/right/internal
. Затем мы работали с регистрами представлений системы счисления, представлениями булевых чисел, а также форматированием чисел с плавающей точкой. Вам стоит поработать со всеми этими возможностями, чтобы получше ознакомиться с ними.


$ ./formatting

================

123456

      123456

      123456

================

123456__________

__________123456

__________123456

================

0x123abc

     0x123abc

0x     123abc

================

0123456

     0123456

     0123456

A hex number with upper case letters: 0X123ABC

A number: 0X64

Oops. now in decimal again: 100

true/false values: 1, 0

true/false values: true, false

doubles: 12.3, 12, 12.0000

scientific double: 1.230000E+11

fixed    double: 123000000000.123001

Very precise double: 0.0000000001

Less precise double: 0.0


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

Все эти stream выражения,

<< foo << bar
, иногда довольно длинные и способны запутать читателя: ему может быть непонятно, что именно делает каждое из них. Поэтому взглянем на таблицу, в которой приведены существующие модификаторы форматов (табл. 7.1). Эти модификаторы нужно помещать в выражение
input_stream >> modifier
или
output_stream << modifier
, в этом случае они будут влиять на входные или выходные данные.

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

При взаимодействии, однако, мы уже могли заметить, что большинство модификаторов являются стойкими. Термин «стойкий» означает следующее: после применения они станут влиять на входные/выходные данные до тех пор, пока не будут сброшены. Единственные нестойкие модификаторы в табл. 7.1 —

setw
и
quoted
. Они влияют только на следующий элемент входных/выходных данных. Это важно знать, поскольку при выводе неких данных с определенным форматированием нужно сбрасывать настройки форматирования объекта потока, так как следующий блок выходных данных, создаваемый несвязанным кодом, может выглядеть странно. Это же верно и для преобразования входных данных, где правильный ход программы может быть нарушен из-за неверных настроек манипулятора ввода/вывода.

Мы не использовали следующие манипуляторы, поскольку они никак не связаны с форматированием, но для полноты картины рассмотрим и их (табл. 7.2).

Среди перечисленных модификаторов стойкими являются только

skipws/noskipws
и
unitbuf/nounitbuf
.

Инициализируем сложные объекты из файла вывода

Считывать отдельные числа и слова довольно просто, поскольку оператор

>>
имеет перегруженные версии для всех этих типов, а потоки ввода удобным образом отбрасывают все промежуточные пробелы.

Но что, если перед нами более сложная структура и нужно прочесть ее из потока ввода или же требуется считать строки, состоящие более чем из одного слова (по умолчанию они будут разбиты на отдельные слова из-за того, что пробелы опускаются)?

Для любого типа можно предоставить еще одну перегруженную версию оператора потока ввода

>>
, и сейчас мы увидим воплощение этого на практике.


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

В этом примере мы определим пользовательскую структуру данных и предоставим возможности для чтения ее объектов из потоков ввода.


1. Включим некоторые заголовочные файлы и объявим об использовании пространства имен

std
для удобства:


#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;


2. В качестве примера сложного объекта определим структуру

city
. Она будет иметь название, количество населения и географические координаты:


struct city {

  string name;

 size_t population;

 double latitude;

 double longitude;

};


3. Чтобы считать объект такой структуры из последовательного потока ввода, следует перегрузить оператор потоковой функции

>>
. В этом операторе сначала опустим все пробелы, стоящие перед текстом, с помощью
ws
, поскольку пробелы не должны засорять название города. Затем считаем целую строку текста. Это подразумевает, что входной файл будет включать строку, в которой записано только название города. Затем, после символа новой строки, будет следовать список чисел, разделенный запятой, в котором содержится информация о численности населения, а также географическая широта и долгота:


istream& operator>>(istream &is, city &c)

{

  is >> ws; getline(is, c.name);

 is >> c.population

   >> c.latitude

   >> c.longitude; return is;

}


4. В нашей функции

main
создаем вектор, в котором может содержаться целый диапазон элементов типа
city
. Заполним его с помощью
std::copy
. Входными данными для вызова copy является диапазон
istream_iterator
. Передавая ему тип структуры
city
в качестве параметра шаблона, мы будем использовать перегруженную функцию
>>
, реализованную только что:


int main()

{

  vector l;

 copy(istream_iterator{cin}, {},

     back_inserter(l));


5. Чтобы увидеть, прошло ли преобразование правильно, выведем на экран содержимое списка. Форматирование ввода/вывода,

left << setw(15) <<
, приводит к тому, что название города заполняется пробелами, поэтому выходные данные представлены в приятной и удобочитаемой форме:


for (const auto &[name, pop, lat, lon] : l) {

 cout << left << setw(15) << name

    << " population=" << pop

    << " lat=" << lat

    << " lon=" << lon << '\n';

  }

}


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


Braunschweig

250000 52.268874 10.526770

Berlin

4000000 52.520007 13.404954

New York City

8406000 40.712784 -74.005941

Mexico City

8851000 19.432608 -99.133208


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


$ cat cities.txt | ./initialize_complex_objects

Braunschweig  population=250000 lat=52.2689 lon=10.5268

Berlin     population=4000000 lat=52.52 lon=13.405

New York City population=8406000 lat=40.7128 lon=-74.0059

Mexico City   population=8851000 lat=19.4326 lon=-99.1332


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

Мы снова рассмотрели короткий пример. В нем мы лишь создали новую структуру

city
, а затем перегрузили оператор
>>
итератора
std::istream
для данного типа. Это позволило десериализовать элементы типа
city
, полученные из стандартного потока ввода, с помощью
istream_iterator
.

Открытым может оставаться вопрос, связанный с проверкой на ошибки. Поэтому снова рассмотрим реализацию оператора

>>
:


istream& operator>>(istream &is, city &c)

{

  is >> ws;

 getline(is, c.name);

  is >> c.population >> c.latitude >> c.longitude;

 return is;

}


Мы считываем множество разных элементов. Что произойдет, если один из них даст сбой, а следующий за ним — нет? Означает ли это потенциальное считывание всех следующих элементов со «смещением» в потоке токенов? Нет, этого не произойдет. Если хотя бы один элемент потока ввода не сможет быть преобразован, то объект потока ввода входит в ошибочное состояние и отказывается выполнять дальнейшие преобразования. Это значит, что если, например,

c.population
или
c.latitude
не могут быть преобразованы, то остальные операнды
>>
будут просто отброшены и мы покинем область действия функции оператора с наполовину заполненным объектом.

На вызывающей стороне мы получим оповещение об этом при написании конструкции

if (input_stream >> city_object)
. Такое потоковое выражение неявно преобразуется в булево значение, когда используется как условное выражение. Оно возвращает значение
false
, если объект потока ввода находится в ошибочном состоянии. Зная это, можно сбросить поток и выполнить подходящие операции.

В данном примере мы не писали подобных условий

if
сами, поскольку позволили выполнить десериализацию итератору
std::istream_iterator
. Реализация перегруженной версии оператора
++
для этого итератора также выполняет проверку ошибок во время преобразования. При генерации ошибок преобразование будет приостановлено. В этом состоянии проверка будет возвращать значение
true
при сравнении с конечным итератором, что заставляет алгоритм copy завершить работу. Таким образом, мы в безопасности.

Заполняем контейнеры с применением итераторов std::istream

В предыдущем примере вы узнали, как можно собрать сложные структуры из потока ввода, а затем заполнить списки или векторы полученными элементами.

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

std::map
. Проблема заключается в том, что мы не можем просто заполнить отдельную структуру значениями и отправить ее в линейный контейнер наподобие списка или вектора, поскольку в ассоциативных массивах полезная нагрузка распределяется между ключом и значением. Однако, как вы увидите, само решение задачи отличается незначительно.

После изучения этого примера вы сможете легко выполнять сериализацию и десериализацию сложных структур данных.


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

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


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;


2. Мы хотим создать небольшую базу данных интернет-мемов. Предположим, мем имеет название, описание и год, в который он родился или был создан. Сохраним их в контейнере

std::map
, где название выступит в качестве ключа, а другая информация будет помещена в структуру как значение, связанное с ним:


struct meme {

  string description;

 size_t year;

};


3. Сначала проигнорируем ключ и просто реализуем перегруженную функцию потокового оператора

>>
для структуры
meme
. Предположим, что описание мема окружено кавычками, за ним следует год. Это будет выглядеть так:
"some description" 2017
. Описание окружено кавычками, так что может содержать пробелы, поскольку мы знаем, что все символы, стоящие между кавычками, принадлежат описанию. После чтения с помощью конструкции
is >> quoted(m.description)
кавычки автоматически используются как разделители и впоследствии отбрасываются, что очень удобно. Сразу после этого считываем число, которое представляет собой год:


istream& operator>>(istream &is, meme &m) {

 return is >> quoted(m.description) >> m.year;

}


4. О’кей, теперь примем в расчет название мема в качестве ключа ассоциативного массива. Чтобы вставить мем в массив, нужно создать объект типа

std::pair<тип_ключа, тип_значения>
. Типом ключа является
string
, а типом значения —
meme
. В названии мема также могут находиться пробелы, поэтому мы используем ту же оболочку, которую применяли для описания.
p.first
— это название, а
p.second
— целая структура
meme
, связанная с ним. Данный объект будет передан другой реализации оператора
>>
, которую мы только что создали:


istream& operator >>(istream &is,

           pair &p) {

  return is >> quoted(p.first) >> p.second;

}


5. На этом все. Напишем функцию

main
, в которой будет создаваться ассоциативный массив, и заполним его. Поскольку мы переопределили оператор
>>
, итератор
istream_iterator
может работать с данным типом непосредственно. Мы позволим ему десериализовать наши объекты типа
meme
, полученные из стандартного потока ввода, и используем итератор вставки, чтобы поместить их в ассоциативный массив:


int main()

{

  map m;

 copy(istream_iterator>{cin},

     {},

     inserter(m, end(m)));


6. Прежде чем вывести на экран все, что у нас есть, сначала найдем самое длинное название мема в ассоциативном массиве. Для этого воспользуемся алгоритмом

std::accumulate
. Он получит исходное значение
0u
(u расшифровывается как unsigned — «беззнаковый») и пройдет по всем элементам ассоциативного массива, чтобы слить их воедино. С точки зрения алгоритма
accumulate
слияние обычно означает сложение. В нашем случае требуется не численная сумма, а размер самой длинной строки. Для получения этого значения предоставим алгоритму вспомогательную функцию
max_func
, которая принимает переменную, содержащую текущий максимальный размер строки (она должна быть беззнаковой, поскольку длина строки знака не имеет), и сравнивает ее с длиной названия текущего мема, чтобы взять максимальное значение. Это делается для каждого элемента. Итоговое возвращаемое значение функции
accumulate
представляет собой максимальную длину названия мема:


  auto max_func ([](size_t old_max,

           const auto &b) {

   return max(old_max, b.first.length());

  });

  size_t width {accumulate(begin(m), end(m),

              0u, max_func)};


7. Теперь быстро пройдем по ассоциативному массиву и выведем каждый элемент. Чтобы выходные данные смотрелись более «аккуратно», воспользуемся конструкцией

<< left << setw(width)
:


  for (const auto &[meme_name, meme_desc] : m) {

   const auto &[desc, year] = meme_desc;

   cout << left << setw(width) << meme_name

     << " : " << desc

     << ", " << year << '\n';

  }

}


8. На этом все. Нам нужна небольшая база данных интернет-мемов, так что заполним текстовый файл некоторыми примерами:


"Doge" "Very Shiba Inu. so dog. much funny. wow." 2013

"Pepe" "Anthropomorphic frog" 2016

"Gabe" "Musical dog on maximum borkdrive" 2016

"Honey Badger" "Crazy nastyass honey badger" 2011

"Dramatic Chipmunk" "Chipmunk with a very dramatic look" 2007


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


$ cat memes.txt | ./filling_containers

Doge        : Very Shiba Inu. so dog. much funny. wow., 2013

Dramatic Chipmunk : Chipmunk with a very dramatic look, 2007

Gabe        : Musical dog on maximum borkdrive, 2016

Honey Badger    : Crazy nastyass honey badger, 2011

Pepe        : Anthropomorphic frog, 2016


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

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

std::map
. Еще одна — мы использовали эти манипуляторы потока. Последняя — вызов
accumulate
, который определяет размер самой длинной строки.

Начнем с работы с ассоциативным массивом. Наша структура

meme
содержит только поле с описанием и год. Название интернет-мема не является частью структуры, поскольку используется в качестве ключа. При вставке чего-нибудь в ассоциативный массив можно предоставить объект типа
std::pair
, имеющий разные типы для ключа и значения. Именно это мы и сделали. Сначала реализовали оператор потока
>>
для структуры
meme
, а затем сделали то же самое для типа
pair
. Затем мы использовали конструкцию
istream_iterator>{cin}
, чтобы получить такие элементы из стандартного потока ввода и передать их в ассоциативный массив с помощью
inserter(m, end(m))
.

При десериализации элементов типа

meme
из потока мы разрешили использовать пробелы в названиях и описаниях. Это легко реализовать, однако они не длиннее одной строки, поскольку мы поместили данные поля в кавычки. Взгляните на пример формата строк:
"Name with spaces" "Description with spaces" 123
.

При работе со строками, заключенными в кавычки как на входе, так и на выходе, поможет

std::quoted
. Если у нас есть строка
s
, то при выводе ее с помощью
cout << quoted(s)
она будет заключена в кавычки. В случае десериализации строки из потока, например используя
cin >> quoted(s)
, мы считаем следующий символ кавычек, заполним строку символами, стоящими после него, и будем делать это до тех пор, пока не увидим следующий символ кавычек, независимо от количества встреченных пробелов.

Последний необычный момент заключается в том, как мы передали функцию

max_func
в наш вызов алгоритма
accumulate
:


auto max_func ([](size_t old_max, const auto &b) {

 return max(old_max, b.first.length());

});

size_t width {accumulate(begin(m), end(m), 0u, max_func)};


Похоже, что функция

max_func
принимает аргумент типа
size_t
и еще один аргумент с автоматическим типом, который оказывается элементом типа
pair
, взятым из ассоциативного массива. На первый взгляд все выглядит очень странно, поскольку большинство бинарных функций сжатия принимают аргументы идентичных типов, а затем объединяют их с помощью некой операции, как, например, это делает
std::plus
. В нашем случае все выглядит по-другому, поскольку мы не объединяем сами пары. Мы только получаем длину каждой строки, представляющей собой ключ, для каждой пары, отбрасываем остальное, а затем сжимаем полученные значения типа
size_t
с помощью функции
max
.

В вызове

accumulate
первый вызов функции
max_func
получает значение
0u
, изначально предоставленное нами в качестве левого аргумента, и ссылку на первый элемент типа
pair
с правой стороны. Это дает возвращаемое значение
max(0u, string_length)
, которое станет левым аргументом для следующего вызова, где очередная пара будет представлять собой правый параметр, и т.д.

Выводим любые данные на экран с помощью итераторов std::ostream

Очень легко вывести что-то на экран с помощью потоков вывода, поскольку в STL есть много полезных перегруженных версий оператора

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

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


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

В этом примере мы поработаем с итератором

std::ostream_iterator
, объединив его с новым пользовательским классом, и взглянем на его возможности неявного преобразования, что может помочь при выводе данных на экран.


1. Сначала указываем, какие заголовочные файлы включить, а затем объявляем об использовании пространства имен

std
:


#include 

#include 

#include 

#include 

#include 


using namespace std;


2. Реализуем функцию преобразования, которая соотносит числа и строки. Она будет возвращать строку

"one"
для значения
1
,
"two"
для значения
2
и т.д.:


string word_num(int i) {


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


  unordered_map m {

   {1, "one"}, {2, "two"}, {3, "three"},

   {4, "four"}, {5, "five"}, //...

  };


4. Теперь можно передать в функцию

find
ассоциативного массива, основанного на хеше, аргумент
i
и вернуть то значение, которое она найдет. Если функция ничего не найдет — например, для заданного числа нет перевода, — то вернем строку
"unknown"
:


  const auto match (m.find(i));

  if (match == end(m)) { return "unknown"; }

  return match->second;

};


5. Мы будем работать также со структурой

bork
. Она содержит только одно целое число и неявно создается на основе целого числа. Кроме того, она имеет функцию
print
, которая принимает ссылку на поток вывода и выводит строку
"bork"
столько раз, сколько указано в целочисленной переменной
borks
:


struct bork {

  int borks;

  bork(int i) : borks{i} {}


  void print(ostream& os) const {

   fill_n(ostream_iterator{os, " "},

      borks, "bork!"s);

  }

};


6. Для использования функции

bork::print
перегрузим оператор
<<
для объектов потока, чтобы они автоматически вызывали функцию
bork::print
, когда объекты типа
bork
попадают в поток вывода:


ostream& operator<<(ostream &os, const bork &b) {

 b.print(os);

  return os;

}


7. Теперь наконец можем начать реализовывать саму функцию

main
. Изначально просто создаем вектор, содержащий некоторые значения:


int main()

{

  const vector v {1, 2, 3, 4, 5};


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
по умолчанию:


#include 

#include 


using namespace std;


2. Реализуем класс, содержащий объект файлового потока и указатель на буфер потока.

cout
, представленный как объект потока, имеет внутренний буфер, который можно подменить. В процессе выполнения подобной подмены можно сохранить его исходное состояние, чтобы в будущем иметь возможность отменить это изменение. Мы могли бы найти его тип в справочном материале по С++, но также можем использовать
decltype
, чтобы узнать, какой тип возвращает конструкция
cout.rdbuf()
. Данный прием подходит не для всех ситуаций, но в нашем случае мы получим тип указателя:


class redirect_cout_region

{

  using buftype = decltype(cout.rdbuf());

  ofstream ofs;

  buftype buf_backup;


3. Конструктор нашего класса принимает в качестве единственного параметра строку

filename
. Данная строка используется для инициализации члена файлового потока
ofs
. После этого можно передать его в
cout
в качестве нового буфера потока. Та же функция, что принимает новый буфер, также возвращает указатель на старый, поэтому можно сохранить его, чтобы в будущем восстановить.


public:

  explicit

  redirect_cout_region (const string &filename)

   : ofs{filename},

    buf_backup{cout.rdbuf(ofs.rdbuf())}

  {}


4. Конструктор по умолчанию делает то же, что и предыдущий конструктор. Различие заключается вот в чем: он не открывает никаких файлов. Передача созданного по умолчанию буфера файлового потока в поток

cout
приводит к тому, что
cout
в некотором роде деактивизируется. Он просто будет отбрасывать входные данные, что мы ему передаем. Это также может быть полезно в отдельных ситуациях.


  redirect_cout_region()

   : ofs{},

     buf_backup{cout.rdbuf(ofs.rdbuf())}

  {}


5. Деструктор просто отменяет наше изменение. Когда объект этого класса выходит из области видимости, буфер потока

cout
возвращается в исходное состояние:


  ~redirect_cout_region() {

   cout.rdbuf(buf_backup);

  }

};


6. Создадим функцию, генерирующую множество выходных данных, чтобы с ней можно было работать в дальнейшем:


void my_output_heavy_function()

{

  cout << "some output\n";

  cout << "this function does really heavy work\n";

  cout << "...and lots of it...\n";

  // ...

}


7. В функции

main
сначала создадим совершенно обычные выходные данные:


int main()

{

  cout << "Readable from normal stdout\n";


8. Теперь откроем еще одну область видимости, и первое, что мы в ней сделаем, — создадим экземпляр нового класса с параметром в виде текстового файла.

Файловые потоки открывают файлы в режиме чтения и записи по умолчанию, поэтому создадут для нас данный файл. Любые выходные данные будут перенаправлены в него, однако для вывода данных мы используем cout:


  {

    redirect_cout_region _ {"output.txt"};

   cout << "Only visible in output.txt\n";

   my_output_heavy_function();

  }


9. После того как мы покинем область видимости, файл будет закрыт и выходные данные станут перенаправляться в стандартный поток вывода. Теперь откроем еще одну область видимости, в которой создадим экземпляр того же класса с помощью конструктора по умолчанию. Таким образом, следующая строка нигде не будет видна, она просто отбросится:


  {

   redirect_cout_region _;

   cout << "This output will "

       "completely vanish\n";

  }


10. После того как мы покинем эту область видимости, стандартный поток вывода восстановится и последнюю строку можно будет увидеть на консоли.


  cout << "Readable from normal stdout again\n";

}


11. Компиляция и запуск программы дадут следующий ожидаемый результат. На консоли будут видны только первая и последняя строки:


$ ./log_regions

Readable from normal stdout

Readable from normal stdout again


12. Можно увидеть, что был создан новый файл

output.txt
, который содержит выходные данные, полученные из первой области видимости. Выходные данные, полученные из второй области видимости, пропали без следа:


$ cat output.txt

Only visible in output.txt some output

this function does really heavy work

... and lots of it...


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

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

s
, а мы хотим сохранить его буфер в переменную
a
и установить новый буфер
b
, то данная конструкция будет выглядеть так:
a = s.rdbuf(b)
. Восстановить буфер можно следующим образом:
s.rdbuf(a)
.

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

redirect_ cout_region
:


{

  cout << "print to standard output\n";


  redirect_cout_region la {"a.txt"};

 cout << "print to a.txt\n";

 redirect_cout_region lb {"b.txt"};

 cout << "print to b.txt\n";

}

cout << "print to standard output again\n";


Этот код работает, поскольку объекты разрушаются в порядке, обратном порядку их создания. Концепция, лежащая в основе данного шаблона, который использует тесное связывание между созданием и разрушением объектов, называется «Получение ресурса есть инициализация» (resource acquisition is initialization, RAII).

Следует упомянуть еще один очень важный момент — обратите внимание на порядок инициализации переменных-членов класса

redirect_cout_region
:


class redirect_cout_region {

  using buftype = decltype(cout.rdbuf());


  ofstream ofs;

 buftype buf_backup;


public:

  explicit

  redirect_cout_region(const string &filename)

   : ofs{filename},

    buf_backup{cout.rdbuf(ofs.rdbuf())}

  {}

...


Как видите, член

buf_backup
создается из выражения, которое зависит от
ofs
. Очевидно, это значит следующее:
ofs
нужно инициализировать до
buf_backup
. Что интересно, порядок, в котором инициализируются переменные-члены, не зависит от порядка элементов списка инициализаторов. Порядок инициализации зависит только от порядка объявления членов!


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

Создаем пользовательские строковые классы путем наследования std::char_traits

Класс

std::string
очень полезен. Однако, как только пользователям нужен строковый класс, семантика которого несколько отличается от обычной обработки строк, они пишут собственный класс
string
. Такая идея редко хороша, поскольку не так просто безопасно обработать строки. К счастью, класс
std::string
— лишь специализированное ключевое слово шаблонного класса
std::basic_string
. Данный класс содержит все сложные средства обработки памяти, но не навязывает никаких правил, как копировать строки, сравнивать их и т.д. Эти правила импортируются в
basic_string
, для чего принимается шаблонный параметр, который содержит класс
traits
.

В этом примере мы увидим, как создавать собственные классы типажей и, соответственно, как создавать пользовательские строки, не реализуя повторно все их возможности.


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

В этом примере мы реализуем два разных пользовательских строковых класса:

lc_string
и
ci_string
. Первый создает на основе любых входных данных строки в нижнем регистре. Второй строки не преобразует, но может выполнить сравнение строк независимо от регистра.


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

std
по умолчанию:


#include 

#include 

#include 


using namespace std;


2. Затем снова реализуем функцию

std::tolower
, которая уже определена в
. Уже существующая функция работает хорошо, но она не имеет модификатора
constexpr
. Однако отдельные строковые функции имеют этот модификатор, начиная с C++17, и мы хотим иметь возможность использовать их для нашего собственного строкового класса-типажа. Функция соотносит символы в верхнем регистре с символами в нижнем регистре и оставляет другие символы неизменными:


static constexpr char tolow(char c) {

 switch (c) {

   case 'A'...'Z': return c - 'A' + 'a';

    default: return c;

  }

}


3. Класс

std::basic_string
принимает три шаблонных параметра: тип основного символа, класс-типаж символа и тип
allocator
. В этом разделе мы изменяем только класс-типаж символа, поскольку он определяет поведение строк. Для повторной реализации только того, что должно отличаться от типичного поведения строк, мы явно наследуем от стандартного класса-типажа:


class lc_traits : public char_traits {

public:


4. Наш класс принимает входные строки, но преобразует их в строки в нижнем регистре. Существует функция, которая делает это посимвольно, так что можно поместить здесь нашу функцию

tolow
. Она имеет модификатор
constexpr
, именно поэтому мы и реализовали самостоятельно функцию
tolow
с таким же модификатором
constexpr
:


  static constexpr

  void assign(char_type& r, const char_type& a ) {

   r = tolow(a);

  }


5. Еще одна функция обрабатывает копирование целой строки в отдельный участок памяти. Мы используем вызов

std::transform
, чтобы скопировать все символы из исходной строки в строку — место назначения, и в то же время соотносим каждый символ с его версией в нижнем регистре:


  static char_type* copy(char_type* dest,

              const char_type* src,

             size_t count) {

    transform(src, src + count, dest, tolow);

   return dest;

  }

};


6. Еще один типаж помогает создать строковый класс, эффективно преобразующий строки в нижний регистр. Напишем еще один типаж, который не изменяет полученную строку, но позволяет выполнять сравнение строк независимо от регистра. Снова унаследуем от существующего стандартного класса-типажа для символов и в этот раз переопределим некоторые члены функции:


class ci_traits : public char_traits {

public:


7. Функция

eq
указывает, равны ли два символа. Реализуем такую же функцию, но будем сравнивать версии символов в нижнем регистре. При использовании этой функции символ
'A'
равен символу
'a'
.


  static constexpr bool eq(char_type a, char_type b) {

   return tolow(a) == tolow(b);

}


8. Функция

lt
указывает, меньше ли значение
a
значения
b
. Применим корректный логический оператор, но только после того, как оба символа будут преобразованы к нижнему регистру:


  static constexpr bool lt(char_type a, char_type b) {

   return tolow(a) < tolow(b);

}


9. Последние две функции работали для посимвольного ввода, а следующие две будут работать для строк. Функция compare работает по аналогии со старой функцией

strncmp
. Она возвращает значение
0
при условии, что обе строки равны от начала до позиции, указанной в переменной
count
. Если они отличаются, то функция возвращает отрицательное или положительное число, которое указывает, какая из строк была меньше с точки зрения лексикографии. Определение разности между обоими символами в каждой позиции должно, конечно же, происходить для символов в нижнем регистре. Положительный момент заключается в том, что весь код цикла является частью функции с модификатором
constexpr
, начиная с C++14.


  static constexpr int compare(const char_type* s1,

                 const char_type* s2,

                  size_t count) {

  for (; count; ++s1, ++s2, --count) {

   const char_type diff (tolow(*s1) - tolow(*s2));

   if (diff < 0) { return -1; }

   else if (diff > 0) { return +1; }

  }

  return 0;

}


10. Последняя функция, которую нужно реализовать для нашего строкового класса, не зависящего от регистра, — это функция

find
. Для заданной входной строки
p
и ее длины
count
она определяет позицию символа
ch
. Затем возвращает указатель на первое включение данного символа или
nullptr
, если такого символа нет. Сравнение в этой функции должно выполняться с использованием функции
tolow
, чтобы поиск не зависел от регистра. К сожалению, мы не можем применить функцию
std::find_if
, поскольку она не имеет модификатора
constexpr
, нужно писать цикл самостоятельно.


  static constexpr

  const char_type* find(const char_type* p,

             size_t count,

              const char_type& ch) {

   const char_type find_c {tolow(ch)};

   for (; count != 0; --count, ++p) {

    if (find_c == tolow(*p)) { return p; }

   }

   return nullptr;

  }

};


11. О’кей, с типажами мы закончили. Теперь можно определить два новых строковых типа.

lc_string
означает lower-case string (строка в нижнем регистре).
ci_string
расшифровывается как case-insensitive string (строка, не зависящая от регистра). Оба класса отличаются от класса
std::string
своими классами-типажами для символов:


using lc_string = basic_string;

using ci_string = basic_string;


12. Чтобы позволить потокам вывода принимать эти новые классы для вывода на экран, нужно перегрузить потоковый оператор

<<
:


ostream& operator<<(ostream& os, const lc_string& str) {

 return os.write(str.data(), str.size());

}

ostream& operator<<(ostream& os, const ci_string& str) {

 return os.write(str.data(), str.size());

}


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


int main()

{

  cout << " string: "

    << string{"Foo Bar Baz"} << '\n'

    << "lc_string: "

    << lc_string{"Foo Bar Baz"} << '\n'

    << "ci_string: "

    << ci_string{"Foo Bar Baz"} << '\n';


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


  ci_string user_input {"MaGiC PaSsWoRd!"};

  ci_string password {"magic password!"};


15. Сравним их и выведем на экран сообщение об их совпадении, если это так:


  if (user_input == password) {

   cout << "Passwords match: \"" << user_input

     << "\" == \"" << password << "\"\n";

  }

}


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

lc_string
все символы указаны в нижнем регистре. Сравнение двух строк, различающихся лишь регистром некоторых символов, прошло успешно и дало правильный результат:


$ ./custom_string

  string: Foo Bar Baz

lc_string: foo bar baz

ci_string: Foo Bar Baz

Passwords match: "MaGiC PaSsWoRd!" == "magic password!"


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

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

Сначала взглянем, откуда появился класс

std::string
:


template <

  class CharT,

  class Traits = std::char_traits,

  class Allocator = std::allocator

  >

class basic_string;


Класс

std::string
, по сути, является
std::basic_string
, и данная конструкция разворачивается к виду
std::basic_string, std::allocator>
. О’кей, что означает это длинное описание типа? Идея заключается вот в чем: можно создать строку, которая работает не только с однобайтовыми элементами типа
char
, но и с другими, более крупными типами. Это позволяет создавать строковые типы, способные работать с более широкими диапазонами символов, нежели типичный диапазон символов американской таблицы ASCII. Нам сейчас не нужно обращать на это внимание.

Класс

char_traits
, однако, содержит алгоритмы, которые необходимы классу
basic_string
для корректной работы. Класс
char_traits
умеет сравнивать, искать и копировать символы и строки.

Класс

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

Если мы хотим, чтобы строковый класс вел себя иначе, то можем попробовать повторно использовать по максимуму все возможности, предоставляемые классами

basic_string
и
char_traits
. Именно это и произошло. Мы реализовали два подкласса
char_traits
, которые называются
case_insentitive
и
lower_caser
, и сконфигурировали с их помощью два совершенно новых строковых типа, применяя их в качестве замены стандартному типу
char_traits
.


Чтобы исследовать другие возможности по подстройке под себя класса

basic_string
, обратитесь к документации C++ STL для
std::char_traits
и взгляните на другие его функции, которые можно переопределить.

Токенизация входных данных с помощью библиотеки для работы с регулярными выражениями

При преобразовании строк сложными способами или разбиении их на фрагменты могут помочь регулярные выражения. Они встроены во многие языки программирования, поскольку очень полезны.

Если вы еще не знакомы с регулярными выражениями, то можете, например, прочесть о них в «Википедии». Это определенно расширит ваш кругозор, так как нетрудно заметить, насколько эти выражения полезны при анализе любых текстов. С их помощью можно, например, проверить корректность адреса электронной почты или IP-адреса, найти и извлечь подстроки из больших строк согласно сложному шаблону и т.д.

В этом примере мы извлечем все ссылки из HTML-документа и выведем их на экран пользователя. Код решения данной задачи будет очень коротким, поскольку язык С++ STL поддерживает регулярные выражения, начиная с версии С++11.


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

В примере мы определим регулярное выражение, которое обнаруживает ссылки, и применим его к файлу HTML, чтобы аккуратно вывести все ссылки, найденные в этом файле.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 

#include 

#include 


using namespace std;


2. Позднее сгенерируем итерабельный диапазон данных, который состоит из строк. Эти строки всегда будут составлять пары из ссылки и ее описания. Так что напишем небольшую вспомогательную функцию, аккуратно выводящую их на экран:


template 

void print(InputIt it, InputIt end_it)

{

  while (it != end_it) {


3. На каждом шаге цикла дважды инкрементируем итератор и берем копии ссылки и ее описания, которые он содержит. Между двумя операциями разыменования итератора добавляем еще один охранный блок

if
, из соображений безопасности проверяющий, достигли ли мы конца итерабельного диапазона данных:


    const string link {*it++};

   if (it == end_it) { break; }

   const string desc {*it++};


4. Теперь выведем на экран ссылки и их описания в аккуратном виде:


    cout << left << setw(28) << desc

      << " : " << link << '\n';

 }

}


5. В функции

main
считываем все, что поступает в стандартный поток ввода. Для этого создаем строку из всех данных, поступивших в стандартный поток, с помощью входного потокового итератора. Для предотвращения токенизации, поскольку нам нужны все входные данные, используем модификатор
noskipws
. Этот модификатор деактивизирует пропуск пробелов и токенизацию:


int main()

{

  cin >> noskipws;

  const std::string in {istream_iterator{cin}, {}};


6. Теперь следует определить регулярное выражение, которое описывает, как с нашей точки зрения должна выглядеть ссылка HTML. Скобки

()
внутри регулярного выражения определяют группы. Они являются частями ссылки, к которым нужно получить доступ — URL и его описание:


  const regex link_re {

   "([^<]*)"};


7. Класс

sregex_token_iterator
выглядит точно так же, как и класс
istream_iterator
. Мы передадим ему целую строку, представляющую собой итерабельный диапазон данных, и определенное нами регулярное выражение. Третий параметр
{1,2}
— это список инициализаторов целочисленных значений. Он определяет, что мы хотим итерировать по группам
1
и
2
из полученных им выражений:


  sregex_token_iterator it {

   begin(in), end(in), link_re, {1, 2}};


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

print
, реализованной нами ранее:


  print(it, {});

}


9. Компиляция и запуск программы дадут следующий результат. Я запустил программу

curl
для домашней страницы ISO C++, которая просто загружает HTML-страницу из Интернета. Конечно, я мог и написать
cat some_html_file.html | ./link_extraction
. Использованное нами регулярное выражение довольно жестко определяет представление о том, как должны выглядеть ссылки в документе HTML. В качестве самостоятельной работы можете сделать его более обобщенным.


$ curl -s "https://isocpp.org/blog" | ./link_extraction

Sign In / Suggest an Article : https://isocpp.org/member/login

Register           : https://isocpp.org/member/register

Get Started!         : https://isocpp.org/get-started

Tour             : https://isocpp.org/tour

C++ Super-FAQ         : https://isocpp.org/faq

Blog             : https://isocpp.org/blog

Forums            : https://isocpp.org/forums

Standardization        : https://isocpp.org/std

About             : https://isocpp.org/about

Current ISO C++ status    : https://isocpp.org/std/status

 (...и многие другие...)


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

Регулярные выражения (или коротко regex) очень полезны. Они могут казаться очень сложными, но вам стоит изучить принципы их работы. Короткое регулярное выражение может избавить от необходимости писать множество строк кода, что пришлось бы сделать при выполнении проверки на соответствие вручную.

В данном примере мы сначала создали объект типа регулярных выражений. Мы передали его конструктору строку, которая описывает регулярное выражение. Самое простое регулярное выражение выглядит как

"."
, оно соответствует любому символу, поскольку точка — это специальный символ для регулярного выражения. Выражение
"a"
соответствует только символам
'a'
. Выражение
"ab*"
означает «один символ
а
, а затем ноль или больше символов
b
» и т.д. Регулярные выражения — довольно обширная тема, более подробную информацию можно найти в «Википедии» и на других сайтах и в литературе.

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

A great link
. Нужно получить часть
some_url.com/foo
, а также
A great link
. Мы создали следующее регулярное выражение, которое содержит группы для соответствия подстрокам (рис. 7.2).

Полное совпадение всегда является группой 0. В данном случае это будет вся строка

. Заключенная в кавычки часть
href
, которая содержит URL, — это группа 1. Скобки в регулярном выражении определяют такие группы. Их у нас две. Еще одна группа — фрагмент текста между тегами
и
, содержащий описание ссылки.

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


sregex_token_iterator it {begin(in), end(in), link_re, {1, 2}};


Части

begin
и
end
обозначают нашу входную строку, по которой итератор, работающий с токенами регулярного выражения, будет итерировать и искать все ссылки.
link_re
— это, конечно, сложное регулярное выражение, созданное нами для поиска ссылок. Часть
{1, 2}
— еще один непонятный фрагмент. Он дает итератору команду опускать полное совпадение и возвращать группу 1, затем инкрементировать итератор и возвращать группу 2, а позже, после очередной операции инкремента, находить следующее совпадение в строке. Это разумное поведение освобождает нас от необходимости писать ненужные строки кода.

Давайте рассмотрим еще один пример. Представим регулярное выражение

"a(b*)(c*)"
. Оно будет соответствовать строкам, содержащим символ
a
, после которого идет ноль или больше символов
b
, а затем — ноль или больше символов
c
:


const string s {" abc abbccc "};

const regex re {"a(b*)(c*)"};


sregex_token_iterator it {begin(s), end(s), re, {1, 2}};


print( *it ); // выводит b

++it;

print( *it ); // выводит c

++it;

print( *it ); // выводит bb

++it;

print( *it ); // выводит ccc


Существует также класс

std::regex_iterator
, который генерирует подстроки, находящиеся между совпадениями с регулярными выражениями.

Удобный и красивый динамический вывод чисел на экран в зависимости от контекста

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

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

□ создавать длинные цепочки манипуляторов ввода/вывода, чтобы вывести несколько переменных с конкретным форматированием, может быть утомительно, а код получится не очень читабельным.


По этим причинам многие пользователи не любят потоки ввода/вывода и даже в С++ все еще применяют функцию

print
для форматирования строк.

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


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

В этом примере мы реализуем класс

format_guard
, способный автоматически отменить любые настройки форматирования. Кроме того, напишем тип-оболочку, который может содержать любые значения, но при выводе будет применять особое форматирование, не загружая код лишними манипуляторами ввода/вывода.


1. Сначала включим некоторые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 


using namespace std;


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

format_guard
. В его конструкторе сохраняются флаги форматирования, которые были заданы для
std::cout
в момент создания объекта. Его деструктор восстанавливает их состояние на момент вызова конструктора. Это, по сути, отменяет все настройки форматирования, примененные между вызовами данных методов.


class format_guard {

  decltype(cout.flags()) f {cout.flags()};

public:

  ~format_guard() { cout.flags(f); }

};


3. Еще один небольшой вспомогательный класс называется

scientific_type
. Поскольку это шаблон класса, он может оборачивать любой тип, который будет представлять собой переменную-член. По сути, он ничего не делает.


template 

struct scientific_type {

  T value;

  explicit scientific_type(T val) : value{val} {}

};


4. Можно определить собственные настройки форматирования для любого типа, который ранее был обернут в

scientific_type
, поскольку в случае перегрузки оператора потока
>>
потоковая библиотека будет выполнять совершенно другой код при выводе подобных типов. Таким образом, можно выводить научные значения в научном представлении с плавающей точкой, в верхнем регистре и явным префиксом
+
, если они имеют положительные значения. Кроме того, мы используем наш класс
format_guard
, чтобы очистить все настройки при выходе из функции:


template 

ostream& operator<<(ostream &os, const scientific_type &w) {

  format_guard _;

  os << scientific << uppercase << showpos;

 return os << w.value;

}


5. В функции

main
сначала поработаем с классом
format_guard
. Откроем новую область видимости, получим экземпляр класса, а затем применим некоторые флаги форматирования к потоку вывода
std::cout
:


int main()

{

  {

   format_guard _;

   cout << hex << scientific << showbase << uppercase;

   cout << "Numbers with special formatting:\n";

   cout << 0x123abc << '\n';

   cout << 0.123456789 << '\n';

  }


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

format_guard
сбросит настройки форматирования. Чтобы это протестировать, выведем точно такие же числа снова. Они должны выглядеть по-другому:


  cout << "Same numbers, but normal formatting again:\n";

 cout << 0x123abc << '\n';

  cout << 0.123456789 << '\n';


7. Теперь воспользуемся классом

scientific_type
. Выведем на экран три числа с плавающей точкой в ряд. Второе число обернем в класс
scientific_type
.

Соответственно, оно будет выведено с применением нашего особенного стиля форматирования, а числа, стоящие перед ним и после него, будут иметь форматирование по умолчанию. В то же время мы избежим появления ненужных символов.


  cout << "Mixed formatting: "

    << 123.0 << " "

    << scientific_type{123.0} << " "

    << 123.456 << '\n';

}


8. Компиляция и запуск программы дадут следующий результат. Первые два числа будут выведены с конкретным форматированием. Следующие два будут иметь форматирование по умолчанию — это показывает, что наш класс

format_guard
работает хорошо. Три числа в последних строках также выглядят соответственно нашим ожиданиям. Число, которое стоит посередине, имеет форматирование класса
scientific_type
, остальные же имеют форматирование по умолчанию:


$ ./pretty_print_on_the_fly

Numbers with special formatting:

0X123ABC

1.234568E-01

Same numbers, but normal formatting again:

1194684

0.123457

Mixed formatting: 123 +1.230000E+02 123.456

Перехватываем читабельные исключения для ошибок потока std::iostream

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

if (cin >> foo >> bar >> ...)
. В случае сбоя этой конструкции мы его обработаем. Использование конструкции
try {...} catch...
при преобразовании объектов не кажется очень полезным.

Фактически библиотека для работы с потоками ввода/вывода в C++ существовала уже в те времена, когда язык C++ еще не работал с исключениями. Поддержка исключений была добавлена позже, это объясняет отсутствие их первоклассной поддержки в потоковой библиотеке.

Применение исключений для потоковой библиотеки возможно при конфигурации каждого отдельного объекта потока таким образом, чтобы он генерировал исключение в тот момент, когда входит в ошибочное состояние. К сожалению, пояснения к ошибкам, лежащие в объектах исключений, которые мы будем отлавливать, не стандартизированы. Это приводит к появлению не очень информативных сообщений об ошибках, их мы рассмотрим в данном разделе. Если вы действительно хотите использовать исключения для объектов потока, то можете дополнительно опросить библиотеку языка C для состояния ошибок файловой системы, чтобы получить добавочную информацию.

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


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

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


1. Сначала включим некоторые заголовочные файлы и объявим об использовании пространства имен

std
:


#include 

#include 

#include 

#include 


using namespace std;


2. Для использования объектов потока вместе с исключениями нужно их активизировать. Чтобы объект файлового потока генерировал исключение в том случае, если нужный файл не существует или при преобразовании возникли ошибки, следует установить значения некоторых битов, указывающих на сбой, в маске исключения. Если мы затем сделаем нечто вызывающее сбой, это сгенерирует исключение. Активизируя

failbit
и
badbit
, мы включаем генерацию исключений для ошибок файловой системы и преобразования:


int main()

{

  ifstream f;

  f.exceptions(f.failbit | f.badbit);


3. Теперь можно открыть блок

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


  try {

    f.open("non_existant.txt");

   int i;

    f >> i;

    cout << "integer has value: " << i << '\n';

  }


4. Если хотя бы в одной из этих ситуаций возникнет ошибка, то будет сгенерирован экземпляр

std::ios_base::failure
. Данный объект имеет функцию-член
what()
, которая должна объяснить, что вызвало генерацию исключения. К сожалению, это сообщение не стандартизировано и не слишком информативно. Однако мы можем хотя бы определить, это проблема с файловой системой (например, файл не существует) или же ошибка преобразования. Глобальная переменная
errno
существовала с момента создания C++, и она получает значение ошибки, которое мы сейчас можем получить. Функция
strerror
преобразует номер ошибки в строку, понятную для человека. Если код равен
0
, то значит, у нас возникла не ошибка файловой системы.


  catch (ios_base::failure& e) {

   cerr << "Caught error: ";

   if (errno) {

    cerr << strerror(errno) << '\n';

   } else {

    cerr << e.what() << '\n';

   }

  }

}


5. Компиляция и запуск программы для двух разных сценариев дадут следующий результат. Если нужно открыть существующий файл, но получить из него целое число нельзя, то мы получим сообщение об ошибке

iostream_category
:


$ ./readable_error_msg

Caught error: ios_base::clear: unspecified iostream_category error


6. Если файл не существует, то мы увидим другое сообщение от

strerror(errno)
:


$ ./readable_error_msg

Caught error: No such file or directory


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

Мы увидели, как можно добавить исключения для объекта потока

s
с помощью выражения
s.exceptions(s.failbit | s.badbit)
. Таким образом, здесь мы никак не сможем применить для открытия файла, к примеру, конструктор экземпляра
std::ifstream
, если хотим получать исключение всякий раз, когда открыть этот файл окажется невозможно:


ifstream f {"non_existant.txt"};

f.exceptions(...); // слишком поздно для генерации исключения


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

if
, обрабатывающих ошибки после каждого шага.

Если бы мы попробовали смоделировать разные причины сбоя потоков, то увидели бы, что генерируются одинаковые исключения. Таким образом, можно только понять, когда выдается ошибку, но не узнать о ее конкретном типе. (Это, конечно, неверно для обработки исключений в целом, только для потоковой библиотеки STL.) Именно поэтому мы дополнительно проверяем значение глобальной переменной errno. Она представляет собой древнюю конструкцию, которая использовалась в те времена, когда в языке С++ не было такого понятия, как исключения.

Если любая функция, связанная с системой, встречает ошибочное состояние, то может установить в качестве значения переменной

errno
нечто отличное от
0
(
0
означает отсутствие ошибок), а затем вызывающая сторона сможет прочесть этот номер ошибки и определить, что он означает. Единственная проблема заключается в следующем: если наше приложение многопоточно и все потоки используют функции, которые могут устанавливать значение данной переменной, то мы не можем выяснить, кто именно установил ее значение. Если мы считаем значение
0
, то это не значит, что ошибок нет, — какая-то другая системная функция, работающая в другом потоке, могла столкнуться с ошибкой. К счастью, этот недостаток был устранен в версии C++11, где каждый поток процесса видит собственную переменную
errno
.

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

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

Загрузка...