Язык программирования C++. Пятое издание

Липпман Стенли Б.

Лажойе Жози

Му Барбара Э.

Часть II

Библиотека С++

 

 

С каждым выпуском новой версии языка С++ росла также его библиотека. На самом деле библиотеке посвящено больше двух третей текста нового стандарта. Хоть мы и не можем рассмотреть каждое средство библиотеки подробно, ее основные средства каждый программист С++ должен знать. Эти основные средства мы и рассмотрим в данной части.

Начнем в главе 8 с базовых средств библиотеки IO. Кроме потоков чтения и записи, связанных с окном консоли, библиотека определяет типы, позволяющие читать и писать в именованные файлы и строки в оперативной памяти.

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

В главе 3 мы познакомились с контейнером типа vector. Подробней мы рассмотрим его и другие типы последовательных контейнеров в главе 9, а также изучим больше операций, предоставленных типом string. Строку типа string можно считать специальным контейнером, который содержит только символы. Тип string поддерживает многие, но не все операции контейнеров.

В главе 10 представлены обобщенные алгоритмы. Обычно они работают с диапазоном элементов в последовательном контейнере или с другой последовательностью. Библиотека алгоритмов предоставляет эффективные реализации различных классических алгоритмов, такие как сортировка и поиск, а также другие общие задачи. Например, есть алгоритм copy, который копирует элементы из одной последовательности в другую; алгоритм find, который ищет указанный элемент; и так далее. Алгоритмы обобщены двумя способами: они могут быть применены к различным видам последовательностей, и эти последовательности могут содержать элементы различных типов.

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

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

 

Глава 8

Библиотека ввода и вывода

 

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

Библиотека ввода и вывода определяет также операции чтения и записи значений встроенных типов. Кроме того, такие классы, как string, обычно определяют подобные операции ввода и вывода для работы с объектами данного класса.

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

В предыдущих программах использовалось немало средств библиотеки IO, большинство из них было представлено в разделе 1.2.

• Тип istream (input stream — поток ввода) обеспечивает операции ввода.

• Тип ostream (output stream — поток вывода) обеспечивает операции вывода.

• Объект cin класса istream читает данные со стандартного устройства ввода.

• Объект cout класса ostream записывает данные на стандартное устройство вывода.

• Объект cerr класса ostream записывает данные на стандартное устройство сообщений об ошибке. Объект cerr, как правило, используется для сообщений об ошибках в программе.

• Оператор >> используется для чтения данных, передаваемых в объект класса istream.

• Оператор << используется для записи данных, передаваемых в объект класса ostream.

• Функция getline() (см. раздел 3.2.2) получает ссылку на объект класса istream и ссылку на объект класса string, а затем читает слово из потока ввода в строку.

 

8.1. Классы ввода-вывода

 

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

Для поддержки столь разных видов обработки ввода-вывода, кроме уже использованных ранее типов istream и ostream, библиотека определяет целую коллекцию типов ввода-вывода. Эти типы (табл. 8.1) определены в трех отдельных заголовках: заголовок iostream определяет базовые типы, используемые для чтения и записи в поток, заголовок fstream определяет типы, используемые для чтения и записи в именованные файлы, заголовок sstream определяет типы, используемые для чтения и записи в строки, расположенные в оперативной памяти.

Таблица 8.1. Типы и заголовки библиотеки ввода-вывода 

Заголовок Тип
iostream istream , wistream — читают данные из потока
ostream , wostream — записывают данные в поток
iostream , wiostream — читают и записывают данные в поток
fstream ifstream , wifstream — читают данные из файла
оfstream , wofstream — записывают данные в файл
fstream , wfstream — читают и записывают данные в файл
sstream istringstream , wistringstream — читают данные из строки
ostringstream , wostringstream — записывают данные в строку
stringstream , wstringstream — читают и записывают данные в строку

Для поддержки языков, использующих расширенные символы, библиотека определяет набор типов и объектов, манипулирующих данными типа wchar_t (см. раздел 2.1.1). Имена версий для расширенных символов начинаются с буквы w. Например, объекты wcin, wcout и wcerr соответствуют обычным объектам cin, cout и cerr, но для расширенных символов. Такие объекты определяются в том же заголовке, что и типы для обычных символов. Например, заголовок fstream определяет типы ifstream и wifstream.

Взаимоотношения между типами ввода и вывода

Концептуально ни вид устройства, ни размер символов не влияют на операции ввода-вывода. Например, оператор >> можно использовать для чтения данных из окна консоли, из файла на диске или из строки. Точно так же этот оператор можно использовать независимо от того, читаются ли символы типа char или wchar_t.

Используя наследование (inheritance), библиотека позволяет игнорировать различия между потоками различных видов. Подобно шаблонам (см. раздел 3.3), связанные наследованием классы можно использовать, не вникая в детали того, как они работают. Более подробная информация о наследовании в языке С++ приведена в главе 15 и в разделе 18.3.

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

Типы ifstream и istringstream происходят от класса istream. Таким образом, объект типа ifstream или istringstream можно использовать так, как будто это объект класса istream. Объекты этих типов можно использовать теми же способами, что и объект cin. Например, можно вызвать функцию getline() объекта ifstream или istringstream либо использовать их оператор >> для чтения данных. Точно так же типы ofstream и ostringstream происходят от класса ostream. Следовательно, объекты этих типов можно использовать теми же способами, что и объект cout.

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

 

8.1.1. Объекты ввода-вывода не допускают копирования и присвоения

Как упоминалось в разделе 7.1.3, объекты ввода-вывода не допускают копирования и присвоения:

ofstream out1, out2;

out1 = out2;        // ошибка: нельзя присваивать потоковые объекты

ofstream print(ofstream); // ошибка: нельзя инициализировать параметр

                          // типа ofstream

out2 = print(out2); // ошибка: нельзя копировать потоковые объекты

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

 

8.1.2. Флаги состояния

В связи с наследованием классов ввода-вывода возможно возникновение ошибок. Некоторые из ошибок исправимы, другие происходят глубоко в системе и не могут быть исправлены в области видимости программы. Классы ввода-вывода определяют функции и флаги, перечисленные в табл. 8.2, позволяющие обращаться к флагам состояния (condition state) потока и манипулировать ими.

Таблица 8.2. Флаги состояния библиотеки ввода-вывода

strm ::iostate strm — один из типов ввода-вывода, перечисленных в табл. 8.1. iostate — машинно-зависимый целочисленный тип, представляющий флаг состояния потока
strm ::badbit Значение флага strm ::iostate указывает, что поток недопустим
strm ::failbit Значение флага strm ::iostate указывает, что операция ввода- вывода закончилась неудачей
strm ::eofbit Значение флага strm ::iostate указывает, что поток достиг конца файла
strm ::goodbit Значение флага strm ::iostate указывает, что поток не находится в недопустимом состоянии. Это значение гарантированно будет нулевым
s .eof() Возвращает значение true , если для потока s установлен флаг  eofbit
s .fail() Возвращает значение true , если для потока s установлен флаг  failbit
s .bad() Возвращает значение true , если для потока s установлен флаг  badbit
s .good() Возвращает значение true , если поток s находится в допустимом состоянии
s .clear() Возвращает все флаги потока s в допустимое состояние
s .clear( флаг ) Устанавливает определенный флаг (флаги) потока s в допустимое состояние. Флаг имеет тип strm ::iostate
s .setstate( флаг ) Добавляет в поток s определенный флаг. Флаг имеет тип  strm ::iostate
s .rdstate() Возвращает текущее состояние потока s как значение типа  strm ::iostate

В качестве примера ошибки ввода-вывода рассмотрим следующий код:

int ival;

cin >> ival;

Если со стандартного устройства ввода ввести, например, слово Boo , то операция чтения потерпит неудачу. Оператор ввода ожидал значение типа int, но получил вместо этого символ В. В результате объект cin перешел в состояние ошибки. Точно так же объект cin окажется в состоянии ошибки, если ввести символ конца файла.

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

while (cin >> word)

 // ok: операция чтения успешна ...

Условие оператора while проверяет состояние потока, возвращаемого выражением >>. Если данная операция ввода успешна, состояние остается допустимым и условие выполняется.

Опрос состояния потока

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

Библиотека ввода-вывода определяет машинно-зависимый целочисленный тип iostate, используемый для передачи информации о состоянии потока. Этот тип используется как коллекция битов, подобно переменной quiz1 в разделе 4.8. Классы ввода-вывода определяют четыре значения constexpr (разделе 2.4.4) типа iostate, представляющие конкретные битовые схемы. Эти значения используются для указания конкретных видов состояний ввода-вывода. Они используются с побитовыми операторами (см. раздел 4.8) для проверки или установки нескольких флагов за раз.

Флаг badbit означает отказ системного уровня, такой как неисправимая ошибка при чтении или записи. Как только флаг badbit установлен, использовать поток обычно больше невозможно. Флаг failbit устанавливается после исправимой ошибки, такой как чтение символа, когда ожидались числовые данные. Как правило, такие проблемы вполне можно исправить и продолжить использовать поток. Достижение конца файла устанавливает флаги и eofbit и failbit. Флаг goodbit, у которого гарантированно будет значение 0, не означает отказа в потоке. Если любой из флагов badbit, failbit или eofbit будет установлен, то оценивающее данный поток условие окажется ложным.

Библиотека определяет также набор функций для опроса состояния этих флагов. Функция good() возвращает значение true, если ни один из флагов ошибок не установлен. Функции bad(), fail() и eof() возвращает значение true, когда установлен соответствующий бит. Кроме того, функция fail() возвращает значение true, если установлен флаг badbit. Корректный способ определения общего состояния потока подразумевал бы использование функции good() или fail(). На самом деле код проверки потока в условии эквивалентен вызову !fail(). Функции bad() и eof() оповещают только о конкретной ошибке.

Управление флагами состояния

Функция-член rdstate() возвращает значение типа iostate, соответствующее текущему состоянию потока. Функция setstate() позволяет установить указанные биты состояния, чтобы указать возникшую проблему. Функция clear() перегружена (см. раздел 6.4): одна ее версия не получает никаких аргументов, а вторая получает один аргумент типа iostate.

Версия функции clear(), не получающая никаких аргументов, сбрасывает все биты отказа. После ее вызова функция good() возвращает значение true. Эти функции-члены можно использовать следующим образом:

// запомнить текущее состояние объекта cin

auto old_state = cin.rdstate();

cin.clear();             // сделать объект cin допустимым

process_input(cin);      // использовать объект cin

cin.setstate(old_state); // вернуть объект cin в прежнее состояние

Версия функции clear(), получающая аргумент, ожидает значение типа iostate, представляющее новое состояние потока. Для сброса отдельного флага используется функция-член rdstate() и побитовые операторы, позволяющие создать новое желаемое состояние.

Например, следующий код сбрасывает биты failbit и badbit, а бит eofbit оставляет неизменным:

// сбросить биты failbit и badbit, остальные биты оставить неизменными

cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

Упражнения раздела 8.1.2

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

Упражнение 8.2. Проверьте созданную функцию, передав ей при вызове объект cin в качестве аргумента.

Упражнение 8.3. В каких случаях завершится следующий цикл while?

while (cin >> i) /* ... */

 

8.1.3. Управление буфером вывода

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

os << "please enter a value: ";

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

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

• Программа завершается нормально. Все буфера вывода освобождаются при выходе из функции main().

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

• Сброс буфера можно осуществить явно, использовав такой манипулятор, как endl (см. раздел 1.2).

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

• Поток вывода может быть связан с другим потоком. В таком случае буфер привязанного потока сбрасывается при каждом чтении или записи другого потока. По умолчанию объекты cin и cerr привязаны к объекту cout. Следовательно, чтение из потока cin или запись в поток cerr сбрасывает буфер потока cout.

Сброс буфера вывода

В приведенных ранее программах уже не раз использовался манипулятор endl, который записывает символ новой строки и сбрасывает буфер. Существуют еще два подобных манипулятора: flush и ends. Манипулятор flush используется для сброса буфер потока без добавления символов в вывод. Манипулятор ends добавляет в буфер нулевой символ, а затем сбрасывает его.

cout << "hi!" << endl;  // выводит hi, новую строку и сбрасывает буфер

cout << "hi!" << flush; // выводит hi и сбрасывает буфер, ничего не

                        // добавляя

cout << "hi!" << ends;  // выводит hi, нулевой символ

                        //  и сбрасывает буфер

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

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

cout << unitbuf;   // при любой записи буфер будет сброшен немедленно

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

cout << nounitbuf; // возвращение к обычной буферизации

Внимание! При сбое программы буфер не сбрасывается

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

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

Связывание потоков ввода и вывода

Когда поток ввода связан с потоком вывода, любая попытка чтения данных из потока ввода приведет к предварительному сбросу буфера, связанного с потоком вывода. Библиотечные объекты cout и cin уже связаны, поэтому оператор cin >> ival; заставит сбросить буфер, связанный с объектом cout.

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

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

Вторая версия функции tiе() получает указатель на объект класса ostream и связывает себя с ним. Таким образом, код x.tie(&o) связывает поток x с потоком вывода o.

Объект класса istream или ostream можно связать с другим объектом класса ostream:

cin.tie(&cout); // только для демонстрации: библиотека

                // автоматически связывает объекты cin и cout

// old_tie указывает на поток (если он есть),

//  в настоящее время связанный с объектом cin

ostream *old_tie = cin.tie(nullptr); // объект cin больше не связан

// связь cin и cerr; не лучшая идея, поскольку объект cin должен быть

// привязан к объекту cout

cin.tie(&cerr);   // чтение в cin сбрасывает объект cerr, а не cout

cin.tie(old_tie); // восстановление обычной связи между cin и cout

Чтобы связать данный поток с новым потоком вывода, функции tie() передают указатель на новый поток. Чтобы разорвать существующую связь, достаточно передать в качестве аргумента значение 0. Каждый поток может быть связан одновременно только с одним потоком. Однако несколько потоков могут связать себя с тем же объектом ostream.

 

8.2. Ввод и вывод в файл

 

В заголовке fstream определены три типа, поддерживающие операции ввода и вывода в файл: класс ifstream читает данные из указанного файла, класс ofstream записывает данные в файл, класс fstream читает и записывает данные в тот же файл. Использование того же файла для ввода и вывода рассматривается в разделе 17.5.3.

Эти типы поддерживают те же операции, что и описанные ранее объекты cin и cout. В частности, для чтения и записи в файлы можно использовать операторы ввода-вывода (<< и >>), можно использовать функцию getline() (см. раздел 3.2.2) для чтения из потока ifstream. Материал, изложенный в разделе 8.1, относится также и к этим типам.

Кроме поведения, унаследованного от типа iostream, определенные в заголовке fstream типы имеют в дополнение члены для работы с файлами, связанными с потоком. Эти операции перечислены в табл. 8.3, они могут быть вызваны для объектов классов fstream, ifstream или ofstream, но не других типов ввода-вывода.

Таблица 8.3. Операции, специфические для типов заголовка fstream

fstream fstrm; Создает несвязанный файловый поток, fstream — это один из типов, определенных в заголовке fstream
fstream fstrm( s ); Создает объект класса fstream и открывает файл по имени s . Параметр s может иметь тип string или быть указателем на символьную строку в стиле С (см. раздел 3.5.4). Эти конструкторы являются явными (см. раздел 7.5.4). Заданный по умолчанию режим файла зависит от типа fstream
fstream fstrm( s , режим ); Подобен предыдущему конструктору, но открывает файл s в указанном режиме
fstrm.open( s ) fstrm.open( s ,  режим ) Открывает файл s и связывает его с потоком fstrm . Параметр s может иметь тип string или быть указателем на символьную строку в стиле С. Заданный по умолчанию режим файла зависит от типа fstream . Возвращает тип void
fstrm.close() Закрывает файл, с которым связан поток fstrm . Возвращает тип void
fstrm.is_open() Возвращает значение типа bool , указывающее, был ли связанный с потоком fstrm файл успешно открыт и не был ли он закрыт 

 

8.2.1. Использование объектов файловых потоков

Когда необходимо читать или писать в файл, определяется объект файлового потока (file stream), который связывается с файлом. Каждый класс файлового потока определяет функцию-член open(), которая выполняет все системные операции, необходимые для поиска указанного файла и его открытия для чтения или записи.

При создании файлового потока можно (но не обязательно) указать имя файла. При предоставлении имени файла функция open() вызывается автоматически:

ifstream in(ifile); // создать объект ifstream и открыть указанный файл

ofstream out;       // файловый поток вывода, не связанный с файлом

Этот код определяет in как входной поток, инициализированный для чтения из файла, указанного строковым аргументом ifile. Код определяет out как поток вывода, который еще не связан с файлом. По новому стандарту имена файлов могут быть переданы как в переменной библиотечного типа string, так и в символьном массиве в стиле С (см. раздел 3.5.4). Предыдущие версии библиотеки допускали только символьные массивы в стиле С.

Использование fstream вместо iostream&

Как упоминалось в разделе 8.1, объект производного типа можно использовать в тех местах, где ожидается объект базового типа. Благодаря этому факту функции, получающие ссылку (или указатель) на один из типов iostream, могут быть вызваны от имени соответствующего типа fstream (или sstream). Таким образом, если имеется функция, получающая ссылку ostream&, то ее можно вызвать, передав объект типа ofstream, то же относится к ссылке istream& и типу ifstream.

Например, функции read() и print() (см. раздел 7.1.3) можно использовать для чтения и записи в именованный файл. В этом примере подразумевается, что имена файлов ввода и вывода передаются как аргументы функции main() (см. раздел 6.2.5):

ifstream input (argv[1]На самом деле препроцессору. Компилятор получит готовый промежуточный файл, в состав которого войдет содержимое подключенной библиотеки. — Примеч. ред .
); // открыть файл транзакций продаж

ofstream output(argv[2]А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
); // открыть файл вывода

Sales_data total;         // переменная для хранения текущей суммы

if (read(input, total)) { // прочитать первую транзакцию

 Sales_data trans;        // переменная для хранения данных следующей

                          // транзакции

 while(read(input, trans)) { // читать остальные транзакции

  if (total.isbn() == trans.isbn()) // проверить isbn

   total.combine(trans); // обновить текущую сумму

  else {

   print(output, total) << endl; // отобразить результат

   total = trans; // обработать следующую книгу

  }

 }

 print (output, total) << endl; // отобразить последнюю транзакцию

} else                          // ввода нет

 cerr << "No data?!" << endl;

Кроме использования именованных файлов, этот код практически идентичен версии программы сложения, приведенной в разделе 7.1.1. Важнейшая часть — вызов функций read() и print(). Этим функциям можно передать объекты типа fstream, хотя типами их параметров определены istream& и ostream& соответственно.

Функции-члены open() и close()

Когда определяется пустой объект файлового потока, вызвав функцию open(), его впоследствии можно связать с файлом:

ifstream in(ifile); // создать объект ifstream и открыть указанный файл

ofstream out;       // файловый поток вывода, не связанный ни с каким

                    // файлом

out.open(ifile + ".copy"); // открыть указанный файл

При неудаче вызова функции open() устанавливается бит failbit (см. раздел 8.1.2). Поскольку вызов функции open() может потерпеть неудачу, имеет смысл проверить ее успешность:

if (out) // проверить успешность вызова функции open

         // вызов успешен, файл можно использовать

Это подобно использованию объекта cin в условии. При неудаче вызова функции open() условие не выполняется и мы не будем пытаться использовать объект in.

Как только файловый поток будет открыт, он остается связанным с определенным файлом. На самом деле вызов функции open() для файлового потока, который уже открыт, приводит к установке бита failbit. Последующие попытки использования этого файлового потока потерпят неудачу. Чтобы связать файловый поток с другим файлом, необходимо сначала закрыть существующий файл. Как только файл закрывается, его можно открыть снова:

in.close();           // закрыть файл

in.open(ifile + "2"); // открыть другой файл

Если вызов функции open() успешен, поток устанавливается в такое состояние, что функция good() возвратит значение true.

Автоматическое создание и удаление

Рассмотрим программу, функция main() которой получает список файлов для обработки (см. раздел 6.2.5). У такой программы может быть следующий цикл:

// для каждого переданного программе файла

for (auto p = argv + 1; p != argv + argc; ++p) {

 ifstream input(*p); // создает input и открывает файл

 if (input) {        // если ошибки с файлом нет, обработать его

  process(input);

 } else

  cerr << "couldn't open: " + string(*p);

} // input выходит из области видимости и удаляется при каждой итерации

При каждой итерации создается новый объект класса ifstream по имени input и открывается файл для чтения. Как обычно, проверяется успех вызова функции open(). Если все в порядке, этот файл передается функции, которая будет читать и обрабатывать ввод. В противном случае выводится сообщение об ошибке.

Поскольку объект input является локальным для цикла while, он создается и удаляется при каждой итерации (см. раздел 5.4.1). Когда объект fstream выходит из области видимости, файл, к которому он привязан, автоматически закрывается. На следующей итерации объект input создается снова.

Когда объект класса fstream удаляется, автоматически вызывается функция close().

Упражнения раздела 8.2.1

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

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

Упражнение 8.6. Перепишите программу книжного магазина из раздела 7.1.1 так, чтобы читать транзакции из файла. Передавайте имя файла как аргумент функции main() (см. раздел 6.2.5).

 

8.2.2. Режимы файла

Каждый поток обладает режимом файла (file mode), определяющим возможный способ использования файла. Список режимов файла и их значений приведен в табл. 8.4.

Таблица 8.4. Режимы файла

in Открывает файл для ввода
out Открывает файл для вывода
app Переходит в конец файла перед каждой записью
ate Переходит в конец файла непосредственно после открытия
trunc Усекает существующий поток при открытии
binary Осуществляет операции ввода-вывода в бинарном режиме

Режим файла можно указать при каждом открытии файла, будь то вызов функции open() или косвенное открытие файла при инициализации потока именем файла. У режимов, которые можно задать, есть ряд ограничений.

• Режим out может быть установлен только для объектов типа ofstream или fstream.

• Режим in может быть установлен только для объектов типа ifstream или fstream.

• Режим trunc может быть установлен, только если устанавливается также режим out.

• Режим app может быть установлен, только если не установлен режим trunc. Если режим app установлен, файл всегда открывается в режиме вывода, даже если это не было указано явно.

• По умолчанию файл, открытый в режиме out, усекается, даже если не задан режим trunc. Чтобы сохранить содержимое файла, открытого в режиме out, необходимо либо задать также режим app, тогда можно будет писать только в конец файла, либо задать также режим in, тогда файл откроется и для ввода, и для вывода. Использование того же файла для ввода и вывода рассматривается в разделе 17.5.3.

• Режимы ate и binary могут быть установлены для объекта файлового потока любого типа и в комбинации с любыми другими режимами.

Для каждого типа файлового потока задан режим файла по умолчанию, который используется в случае, если режим не задан. Файлы, связанные с потоками типа ifstream, открываются в режиме in; файлы, связанные с потоками типа ofstream, открываются в режиме out; а файлы, связанные с потоками типа fstream, открываются в режимах in и out.

Открытие файла в режиме out удаляет существующие данные

По умолчанию при открытии потока типа ofstream содержимое файла удаляется. Единственный способ воспрепятствовать удалению данных файла подразумевает установку режима app:

// file1 усекается в каждом из следующих случаев

ofstream out("file1"); // out и trunc установлены неявно

ofstream out2("file1", ofstream::out); // trunc установлен неявно

ofstream out3("file1", ofstream::out | ofstream::trunc);

// для сохранения содержимого файла следует явно задать режим app

ofstream app("file2", ofstream::app); // out установлен неявно

ofstream app2("file2", ofstream::out | ofstream::app);

Единственный способ сохранить существующие данные в файле, открытом потоком типа ofstream, — это явно установить режим app или in.

Режим файла устанавливается при каждом вызове функции open()

Режим файла некоего потока может изменяться при каждом открытии файла.

ofstream out; // режим файла не установлен

out.open("scratchpad"); // неявно заданы режимы out и trunc

out.close(); // out закрыт, его можно использовать для другого файла

out.open("precious", ofstream::app); // режимы out и app

out.close();

Первый вызов функции open() не задает режим вывода явно; этот файл неявно открывается в режиме out. Как обычно, режим out подразумевает также режим trunc. Поэтому файл scratchpad, расположенный в текущем каталоге, будет усечен. Когда открывается файл precious, задается режим добавления. Все данные остаются в файле, а запись осуществляется в конец файла.

Режим файла устанавливается при каждом вызове функции open() явно или неявно. Когда режим не устанавливается явно, используется значение по умолчанию.

Упражнения раздела 8.2.2

Упражнение 8.7. Пересмотрите программу книжного магазина из предыдущего раздела так, чтобы вывод записывался в файл. Передайте имя этого файла как второй аргумент функции main().

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

 

8.3. Строковые потоки

 

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

Объект класса istringstream читает строку, объект класса ostringstream записывает строку, а объект класса stringstream читает и записывает строку. Подобно типам заголовка fstream, типы, определенные в заголовке sstream, происходят от типов, используемых заголовком iostream. Кроме унаследованных операций, типы, определенные в заголовке sstream, имеют дополнительные члены для работы со строками, связанными с потоком. Эти операции перечислены в табл. 8.5. Они могут быть выбраны для объектов класса stringstream, строковых потоков (string stream), но не других типов ввода-вывода.

Обратите внимание на то, что хотя заголовки fstream и sstream имеют общий интерфейс к заголовку iostream, никакой другой взаимосвязи у них нет. В частности, нельзя использовать функции open() и close() для объектов класса stringstream, а функцию str() нельзя использовать для объектов класса fstream.

Таблица 8.5. Операции, специфические для класса stringstream

sstream strm; strm — несвязанный объект класса stringstream . sstream — это один из типов, определенных в заголовке sstream
sstream strm(s); sstream содержит копию строки s . Этот конструктор является явным (см. раздел 7.5.4).
strm.str() Возвращает копию строки, которую хранит объект strm
strm.str(s) Копирует строку s в объект strm . Возвращает тип void

 

8.3.1. Использование класса

istringstream

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

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

morgan 2015552368 8625550123

drew 9735550130

lee 6095550132 2015550175 8005550000

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

// по умолчанию члены являются открытыми; см. раздел 1.2

struct PersonInfo {

 string name;

 vector phones;

};

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

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

string line, word;         // будут содержать строку и слово из ввода

vector people; // будет содержать все записи из ввода

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

// (или другая ошибка)

while (getline(cin, line)) {

 PersonInfo info; // создать объект для содержания данных записи

 istringstream record(line); // связать запись с читаемой строкой

 record >> info.name;        // читать имя

 while (record >> word)      // читать номер телефона

  info.phones.push_back(word); // и сохранить их

 people.push_back(info);     // добавить эту запись в people

}

Здесь для чтения всей записи со стандартного устройства ввода используется функция getline(). Если вызов функции getline() успешен, то переменная line будет содержать запись из входного файла. В цикле while определяется локальный объект PersonInfo для содержания данных из текущей записи.

Затем с только что прочитанной строкой связывается поток istringstream. Теперь для чтения каждого элемента в текущей записи можно использовать оператор ввода класса istringstream. Сначала читается имя, затем следует цикл while, читающий номера телефонов данного человека.

Внутренний цикл while завершается, когда все данные в строке прочитаны. Этот цикл работает аналогично другим, написанным для чтения из объекта cin. Различие только в том, что этот цикл читает данные из строки, а не со стандартного устройства ввода. Когда строка прочитана полностью, встретившийся "конец файла" свидетельствует о том, что следующая операция ввода в объект record потерпит неудачу.

Внешний цикл while завершается добавлением в вектор только что обработанного объекта класса PersonInfo. Внешний цикл while продолжается, пока объект cin не встретит конец файла.

Упражнения раздела 8.3.1

Упражнение 8.9. Используйте функцию, написанную для первого упражнения 8.1.2, для вывода содержимого объекта класса istringstream.

Упражнение 8.10. Напишите программу для сохранения каждой строки из файла в векторе vector. Затем используйте объект класса istringstream для чтения каждого элемента из вектора по одному слову за раз.

Упражнение 8.11. Программа этого раздела определила свой объект класса istringstream во внешнем цикле while. Какие изменения необходимо внести, чтобы определить объект record вне этого цикла? Перепишите программу, перенеся определение объекта record во вне цикла while, и убедитесь, все ли необходимые изменения внесены.

Упражнение 8.12. Почему в классе PersonInfo не использованы внутриклассовые инициализаторы?

 

8.3.2. Использование класса

ostringstream

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

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

for (const auto &entry : people) { // для каждой записи в people

 ostringstream formatted, badNums; // объекты создаются на каждом

                                   // цикле

 for (const auto &nums : entry.phones) { // для каждого номера

  if (!valid(nums)) {

   badNums << " " << nums; // строка в badNums

  } else

   // "запись" в строку formatted

   formatted << " " << format(nums);

 }

 if (badNums.str().empty()) // если плохих номеров нет

  os << entry.name << " "   // вывести имя

     << formatted.str() << endl; // и переформатированные номера

 else // в противном случае вывести имя и плохие номера

  cerr << "input error: " << entry.name

       << " invalid number(s) " << badNums.str() << endl;

}

В этой программе подразумевается, что есть две функции, valid() и format(), которые проверяют и переформатируют номера телефонов. Интересная часть программы — использование строковых потоков formatted и badNums. Для записи в эти объекты используется обычный оператор вывода (<<). Но они действительно "пишут" строковые манипуляторы. Они добавляют символы к строкам в строковых потоках formatted и badNums соответственно.

Упражнения раздела 8.3.2

Упражнение 8.13. Перепишите программу номеров телефонов из этого раздела так, чтобы читать из именованного файла, а не из объекта cin.

Упражнение 8.14. Почему переменные entry и nums были объявлены как const auto &?

 

Резюме

Язык С++ использует библиотечные классы для обработки потоков ввода и вывода.

• Класс iostream отрабатывает ввод-вывод на консоль.

• Класс fstream отрабатывает ввод-вывод в именованным файл.

• Класс stringstream отрабатывает ввод-вывод в строки в оперативной памяти.

Классы fstream и stringstream связаны происхождением от класса iostream. Классы ввода происходят от класса istream, а классы вывода — от класса ostream. Таким образом, операции, которые могут быть выполнены с объектом класса istream, могут быть также выполнены с объектом класса ifstream или istringstream. Аналогично для классов вывода, происходящих от класса ostream.

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

 

Термины

Классfstream. Файловый поток, обеспечивающий чтение и запись в тот же файл. По умолчанию объект класса ifstreams открывает файл одновременно в режимах in и out.

Классifstream. Файловый поток, читающий данные из файла. По умолчанию поток ifstream открывается в режиме in.

Классistringstream. Строковый поток, читающий данные из строки.

Классofstream. Файловый поток, записывающий данные в файл. По умолчанию поток ofstream открывается в режиме out.

Классostringstream. Строковый поток, записывающий данные в строку.

Классstringstream. Строковый поток, читающий и записывающий данные в строку.

Наследование (inheritance). Программное средство, позволяющее типу наследовать интерфейс другого типа. Классы ifstream и istringstream происходят от классов istream и ofstream, а класс ostringstream происходит от класса ostream. Более подробная информация о наследовании приведена в главе 15.

Режим файла (file mode). Флаги классов заголовка fstream, устанавливаемые при открытии файла и задающие способ его применения. Строковый поток (string stream). Потоковый объект, читающий или записывающий данные в строку. Кроме возможностей, присущих классу iostream, классы строковых потоков определяют перегруженную функцию str(). Вызов функции str() без аргументов возвращает строку, с которой связан объект строкового потока, а ее вызов со строковым аргументом свяжет строковый поток с копией этой строки.

Файловый поток (file stream). Потоковый объект этого класса позволяет читать и записывать данные в именованный файл. Кроме возможностей, присущих классу iostream, класс fstream обладает также функциями-членами open() и close(). Функция-член open() получает символьную строку в стиле С, которая содержит имя открываемого файла и необязательный аргумент, задающий режим. Функция-член close() закрывает файл, с которым связан поток. Ее следует вызвать прежде, чем может быть открыт другой файл.

Флаг состояния (condition state). Флаги и связанные с ними функции потоковых классов позволяют выяснить, пригоден ли данный поток для использования.

 

Глава 9

Последовательные контейнеры

 

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

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

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

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

Эта глава основана на материале разделов 3.2–3.4. Здесь подразумевается, что читатель знаком с их материалом.

 

9.1. Обзор последовательных контейнеров

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

• цена добавления и удаления элементов из контейнера;

• цена непоследовательного доступа к элементам контейнера.

Таблица 9.1. Типы последовательных контейнеров

vector Массив переменного размера (вектор). Обеспечивает быстрый произвольный доступ. Вставка и удаление элементов, кроме как в конец, могут быть продолжительными
deque Двухсторонняя очередь. Обеспечивает быстрый произвольный доступ. Быстрая вставка и удаление в начало и конец
list Двухсвязный список. Обеспечивает только двунаправленный последовательный доступ. Быстрая вставка и удаление в любую позицию
forward_list Односвязный список. Обеспечивает только последовательный доступ в одном направлении. Быстрая вставка и удаление в любую позицию
array Массив фиксированного размера. Обеспечивает быстрый произвольный доступ. Не позволяет добавлять или удалять элементы
string Специализированный контейнер, подобный вектору, содержащий символы. Быстрый произвольный доступ. Быстрая вставка и удаление в конец

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

Например, контейнеры string и vector содержат свои элементы в соседних областях памяти. Поскольку элементы расположены последовательно, их адрес по индексу вычисляется очень быстро. Но добавление или удаление элемента в середину такого контейнера занимает много времени: для обеспечения последовательности все элементы после удаления или вставки придется переместить. Кроме того, добавление элемента может иногда потребовать резервирования дополнительного места для хранения. В этом случае каждый элемент следует переместить в новое место.

Контейнеры list и forward_list разработаны так, чтобы быстро добавлять и удалять элементы в любой позиции контейнера. Однако эти типы не поддерживают произвольный доступ к элементам: обратиться к элементу можно, только перебрав контейнер. Кроме того, дополнительные затраты памяти этих контейнеров зачастую являются существенными по сравнению с контейнерами vector, deque и array.

Контейнер deque — это более сложная структура данных. Как и контейнеры string, vector и deque, они обеспечивают быстрый произвольный доступ. Как и у контейнеров string и vector, добавление или удаление элементов в середину контейнера deque — потенциально дорогая операция. Однако добавление и удаление элементов в оба конца контейнера deque являются быстрой операцией, сопоставимой с таковыми у контейнеров list и forward_list.

Типы forward_list и array были добавлены согласно новому стандарту. Класс array — более безопасная и легкая в употреблении альтернатива встроенным массивам. Как и встроенные массивы, объект библиотечного класса array имеет фиксированный размер. В результате он не поддерживает операции по добавлению и удалению элементов или изменению размеров контейнера. Класс forward_list предназначен для замены наилучших самодельных односвязных списков. Следовательно, у него нет функции size(), поскольку сохранение или вычисление его размера повлекло бы дополнительные затраты по сравнению с самодельным списком. Для других контейнеров функция size() гарантированно будет выполняться быстро с постоянной скоростью.

По причинам, изложенным в разделе 13.6, новые библиотечные контейнеры работают существенно быстрее, чем в предыдущих версиях. Библиотечные контейнеры почти наверняка работают так же (если не лучше), чем даже наиболее тщательно разработанные альтернативы. Современные программы С++ должны использовать библиотечные контейнеры, а не множество примитивных структур, подобных массивам.

Решение о том, какой последовательный контейнер использовать

Как правило, если нет серьезных оснований предпочесть другой контейнер, лучше использовать контейнер vector.

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

• Если нет причин использовать другой контейнер, используйте вектор.

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

• Если нужен произвольный доступ к элементам, используйте контейнер vector или deque.

• Если необходима вставка или удаление элементов в середину, используйте контейнер list или forward_list.

• Если необходима вставка или удаление элементов в начало или в конец, но не в середину, используйте контейнер deque.

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

  • сначала решите, необходимо ли фактически добавлять элементы в середину контейнера. Зачастую проще добавлять элементы в vector, а затем использовать библиотечную функцию sort() (рассматриваемую в разделе 10.2.3) для переупорядочивания контейнера по завершении ввода;

  • если вставка в середину необходима, рассмотрите возможность использования контейнера list на фазе ввода, а по его завершении — копирования списка в вектор.

Но что если нужен и произвольный доступ, и вставка (удаление) элементов в середину контейнера? Решение будет зависеть от отношения цены доступа к элементам в контейнере list и forward_list цены вставки (удаления) элементов в контейнер vector или deque. Как правило, выбор типа контейнера определит преобладающая операция приложения (произвольный доступ или вставка и удаление). В таких случаях, вероятно, потребуется проверка производительности приложения с использованием контейнеров обоих типов.

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

Упражнения раздела 9.1

Упражнение 9.1. Какой из контейнеров (vector, deque или list) лучше подходит для приведенных ниже задач? Объясните, почему. Если нельзя отдать предпочтение тому или иному контейнеру, объясните, почему?

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

• Чтение неизвестного заранее количества слов. Новые слова всегда добавляются в конец. Следующее значение извлекается из начала.

• Чтение неизвестного заранее количества целых чисел из файла. Числа сортируются, а затем выводятся на стандартное устройство вывода.

 

9.2. Обзор библиотечных контейнеров

 

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

• Некоторые функциональные возможности (табл. 9.2) поддерживаются контейнерами всех типов.

• Другие операции являются специфическими только для последовательных (табл. 9.3), ассоциативных (табл. 11.7) или неупорядоченных (табл. 11.8) контейнеров.

• Остальные являются общими лишь для небольшого подмножества контейнеров.

Таблица 9.2. Средства контейнеров

Псевдонимы типов
iterator Тип итератора для контейнера данного типа
const_iterator Тип итератора, позволяющий читать, но не изменять значение элемента
size_type Целочисленный беззнаковый тип, размер которого достаточно велик, чтобы содержать значение размера наибольшего возможного контейнера данного типа
difference_type Целочисленный знаковый тип, размер которого достаточно велик, чтобы содержать значение разницы между двумя итераторами
value_type Тип элемента
reference Тип l-значения элемента; то же, что и value_type&
const_reference Тип константного l-значения элемента; аналог  const value_type&
Конструкторы
С с; Стандартный конструктор, создающий пустой контейнер
С c1(c2); Создает контейнер c1 как копию контейнера c2
С c(b, е); Копирует элементы из диапазона, обозначенного итераторами b и е ( недопустимо для массива )
C c{а, b, c...}; Списочная инициализация контейнера с
Присвоение и замена
c1 = c2 Заменяет элементы контейнера c1 элементами контейнера c2
c1 = {a, b, c...} Заменяет элементы контейнера c1 элементами списка ( недопустимо для массива )
a.swap(b) Меняет местами элементы контейнеров а и b
swap(а, b) Эквивалент a.swap(b)
Размер
c.size() Возвращает количество элементов контейнера c ( недопустимо для контейнера forward_list )
c.max_size() Возвращает максимально возможное количество элементов контейнера с
c.empty() Возвращает логическое значение false , если контейнер c пуст. В противном случае возвращает значение true
Добавление/удаление элементов (недопустимо для массива) Примечание: интерфейс этих функций зависит от типа контейнера
c.insert( args ) Копирует элемент(ы), указанный параметром args , в контейнер c
c.emplace( inits ) Использует параметр inits для создания элемента в контейнере с
c.erase( args ) Удаляет элемент(ы), указанный параметром args , из контейнера c
c.clear() Удаляет все элементы из контейнера c ; возвращает значение void
Операторы равенства и отношения
== , != Равенство допустимо для контейнеров всех типов
< , <= , > , >= Операторы отношения ( недопустимы для неупорядоченных ассоциативных контейнеров )
Получения итераторов
c.begin() , c.end() Возвращают итератор на первый и следующий после последнего элемент в контейнере с
c.cbegin() , c.cend() Возвращают const_iterator
Дополнительные члены реверсивных контейнеров (недопустимы для forward_list )
reverse_iterator Итератор, обеспечивающий доступ к элементам в обратном порядке
const_reverse_iterator Реверсивный итератор, не позволяющий запись в элементы
с.rbegin() , c.rend() Возвращает итератор на последний и следующий после первого элементы контейнера c
c.crbegin() , c.crend() Возвращают итератор const_reverse_iterator

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

Обычно каждый контейнер определяется в файле заголовка, название которого совпадает с именем типа. Таким образом, тип deque определен в заголовке deque, тип list — в заголовке list и т.д. Контейнеры — это шаблоны классов (см. раздел 3.3). Подобно векторам, при создании контейнера специфического типа необходимо предоставить дополнительную информацию. Для большинства контейнеров, но не всех, предоставляемой информацией является тип элемента:

list // список, содержащий объекты класса Sales_data

deque    // двухсторонняя очередь переменных типа double

Ограничения на типы, которые может содержать контейнер

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

vector> lines; // вектор векторов

где lines — это вектор, элементами которого являются векторы строк.

Устаревшие компиляторы могут потребовать пробела между угловыми скобками, например vector >.

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

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

// тип noDefault не имеет стандартного конструктора

vector v1(10, init); // ok: предоставлен инициализатор

                                // элемента

vector v2(10);       // ошибка: необходимо предоставить

                                // инициализатор элемента

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

Упражнения раздела 9.2

Упражнение 9.2. Определите список (list), элементами которого будут двухсторонние очереди целых чисел.

 

9.2.1. Итераторы

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

За одним исключением контейнерные итераторы поддерживают все функции, перечисленные в табл. 3.6. Исключение в том, что итераторы контейнера forward_list не поддерживают оператор декремента (--). Операторы арифметических действий с итераторами, перечисленными в табл. 3.7, применимы только к итераторам контейнеров string, vector, deque и array. К итераторам контейнеров любых других типов эти операторы неприменимы.

Диапазоны итераторов

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

Диапазон итераторов (iterator range) обозначается парой итераторов, каждый из которых указывает на элемент или на следующий элемент после последнего в том же контейнере. Эти два итератора, обозначающие диапазон элементов контейнера, зачастую называют begin и end или, что несколько обманчиво, first и last.

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

Такой диапазон элементов называется интервал, включающий левый элемент (left-inclusive interval). Вот стандартная математическая форма записи такого диапазона:

[ begin, end )

Это указывает, что диапазон начинается с элемента, обозначенного итератором begin, и заканчивается элементом перед тем, который обозначен итератором end. Итераторы begin и end должны относиться к тому же контейнеру. Итератор end может быть равен итератору begin, но не должен указывать на элемент перед обозначенным итератором begin.

Требования к итераторам, формирующим диапазон

Два итератора, begin и end , позволяют задать диапазон при следующих условиях.

• Итераторы относятся к существующим элементам или к следующему элементу за концом того же контейнера.

• Элемент end достижим благодаря последовательному приращению итератора begin . Другими словами, итератор end не должен предшествовать итератору begin .

#_.jpg Компилятор не может сам соблюдать эти требования. Позаботиться об этом придется разработчику.

Смысл использования диапазонов, включающих левый элемент

Библиотека использует диапазоны, включающие левый элемент, потому, что они обладают двумя очень полезными качествами (напомним, что допустимый диапазон обозначают итераторы begin и end).

• Если итератор begin равен итератору end, то диапазон пуст.

• Если итератор begin не равен итератору end, в диапазоне содержится по крайней мере один элемент и итератор begin указывает на первый из них.

• Можно осуществлять инкремент итератора begin до тех пор, пока он не станет равен итератору end (т.е. begin == end).

Благодаря этим качествам можно создавать вполне безопасные циклы обработки диапазона элементов, например, такие:

while (begin != end) {

 *begin = val; // ok: диапазон не пуст, begin обозначает элемент

 ++begin;      // переместить итератор и получить следующий элемент

}

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

Упражнения раздела 9.2.1

Упражнение 9.3. Каким условиям должны удовлетворять итераторы, обозначающие диапазон?

Упражнение 9.4. Напишите функцию, которая получает два итератора вектора vector и значение типа int. Организуйте поиск этого значения в диапазоне и возвратите логическое значение (тип bool), указывающее, что значение найдено.

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

Упражнение 9.6. Что не так со следующей программой? Как ее можно исправить?

list lst1;

list::iterator iter1 = lst1.begin(),

iter2 = lst1.end();

while (iter1 < iter2) /* ... */

 

9.2.2. Типы-члены классов контейнеров

Класс каждого контейнера определяет несколько типов, представленных в табл. 9.2. Три из них уже использовались: size_type (см. раздел 3.2.2), iterator и const_iterator (см. раздел 3.4.1).

Кроме итераторов уже использовавшихся типов, большинство контейнеров предоставляет реверсивные итераторы (reverse_iterator). Другими словами, реверсивный итератор — это итератор, перебирающий контейнер назад и инвертирующий значение его операторов. Например, оператор ++ возвращает реверсивный итератор к предыдущему элементу. Более подробная информация о реверсивных итераторах приведена в разделе 10.4.3.

Остальные псевдонимы типов позволяют использовать тип хранящихся в контейнере элементов, даже не зная его конкретно. Если необходим тип элемента, используется тип value_type контейнера. Если необходима ссылка на этот тип, используется член reference или const_reference. Эти связанные с элементами псевдонимы типов весьма полезны в обобщенном программировании, которое рассматривается в главе 16.

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

// iter имеет тип iterator, определенный классом list

list::iterator iter;

// count имеет тип difference_type, определенный классом vector

vector::difference_type count;

В этих объявлениях оператор области видимости (см. раздел 1.2) позволяет указать, что используется тип-член iterator класса list и тип-член difference_type, определенный классом vector, соответственно.

Упражнения раздела 9.2.2

Упражнение 9.7. Какой тип следует использовать в качестве индекса для вектора целых чисел?

Упражнение 9.8. Какой тип следует использовать для чтения элементов в списке строк?

 

9.2.3. Функции-члены

begin()

и

end()

Функции-члены begin() и end() (см. раздел 3.4.1) возвращают итераторы на первый и следующий после последнего элементы контейнера соответственно. Эти итераторы, как правило, используют при создании диапазона итераторов, охватывающего все элементы контейнера.

Как показано в табл. 9.2, есть несколько версий этих функций: имена которых начинаются с буквы r возвращают реверсивные итераторы (рассматриваются в разделе 10.4.3), а с буквы c — возвращают константную версию соответствующего итератора:

list a = {"Milton", "Shakespeare", "Austen"};

auto it1 = a.begin();  // list::iterator

auto it2 = a.rbegin(); // list::reverse_iterato r

auto it3 = a.cbegin(); // list::const_iterator

auto it4 = a.crbegin();// list::const_reverse_iterator

Функции, имена которых не начинаются с буквы c, перегружены. Таким образом, фактически есть две функции-члена begin(). Одна является константной (см. раздел 7.1.2) и возвращает тип const_iterator контейнера. Вторая не константна и возвращает тип iterator контейнера. Аналогично для функций rbegin(), end() и rend(). При вызове такой функции-члена для неконстантного объекта используется версия, возвращающая тип iterator. Константная версия итераторов будет получена только при вызове этих функций для константного объекта. Подобно указателям и ссылкам на константу, итератор типа iterator можно преобразовать в соответствующий итератор типа const_iterator, но не наоборот.

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

// тип указан явно

list::iterator it5 = a.begin();

list::const_iterator it6 = a.begin();

// iterator или const_iterator в зависимости от типа а

auto it7 = a.begin();  // const_iterator только если a константа

auto it8 = a.cbegin(); // it8 - const_iterator

Когда с функциями begin() или end() используется ключевое слово auto, тип возвращаемого итератора зависит от типа контейнера. То, как предполагается использовать итератор, несущественно. Версии c позволяют получать итератор типа const_iterator независимо от типа контейнера.

Когда доступ на запись не нужен, используйте версии cbegin() и cend().

Упражнения раздела 9.2.3

Упражнение 9.9. В чем разница между функциями begin() и cbegin()?

Упражнение 9.10. Каковы типы следующих четырех объектов?

vector v1;

const vector v2;

auto it1 = v1.begin(), it2 = v2.begin();

auto it3 = v1.cbegin(), it4 = v2.cbegin();

 

9.2.4. Определение и инициализация контейнера

Каждый контейнерный тип определяет стандартный конструктор (см. раздел 7.1.4). За исключением контейнера array стандартный конструктор создает пустой контейнер определенного типа. Также за исключением контейнера array другие конструкторы получают аргументы, которые определяют размер контейнера и исходные значения его элементов.

Инициализация контейнера как копии другого контейнера

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

Таблица 9.3. Определение и инициализация контейнера

С c; Стандартный конструктор. Если C — массив, то элементы контейнера с инициализируются значением по умолчанию; в противном случае контейнер с пуст
С c1(c2) С c1 = c2 Контейнер c1 — копия c2 . Контейнеры c1 и c2 должны быть одинакового типа (т.е. должны иметь тот же тип контейнера и содержать элементы того же типа; у массивов должен также совпадать размер)
С с{a, b, с...} С с = {a, b, с...} Контейнер  c содержит копию элементов из списка инициализации. Тип элементов в списке должен быть совместимым с типом элементов C . В случае массива количество элементов списка не должно превышать размер массива, а все недостающие элементы инициализируются значением по умолчанию (см. раздел 3.3.1)
С с(b, е) Контейнер  c содержит копию элементов из диапазона, обозначенного итераторами b и е . Тип элементов должен быть совместимым с типом элементов С . (Недопустимо для массива.)
Получающие размер конструкторы допустимы только для последовательных контейнеров (исключая массив)
С seq(n) Контейнер seq содержит n элементов, инициализированных значением по умолчанию; этот конструктор является явным (см. раздел 7.5.4). (Недопустимо для строки.)
С seq(n,t) Контейнер seq содержит n элементов со значением t

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

// каждый контейнер имеет три элемента, инициализированных

// предоставленными инициализаторами

list authors = {"Milton", "Shakespeare", "Austen"};

vector articles = {"a", "an", "the"};

list list2(authors);     // ok: типы совпадают

deque authList(authors); // ошибка: типы контейнеров

                                 //  не совпадают

vector words(articles);  // ошибка: типы элементов не совпадают

// ok: преобразует элементы const char* в string

forward_list words(articles.begin(), articles.end());

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

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

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

// копирует до, но не включая элемент, обозначенный итератором it

deque authList(authors.begin(), it);

Списочная инициализация

По новому стандарту контейнер допускает списочную инициализацию (см. раздел 3.3.1):

// каждый контейнер имеет три элемента, инициализированных

// предоставленными инициализаторами

list authors = {"Milton", "Shakespeare", "Austen"};

vector articles = {"a", "an", "the"};

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

Конструкторы последовательных контейнеров, связанные с размером

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

vector ivec(10, -1);     // десять элементов типа int; значение -1

list svec(10, "hi!"); // десять строк; значение "hi!"

forward_list ivec(10);   // десять элементов; значение 0

deque svec(10);       // десять элементов; все пустые строки

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

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

Библиотечные массивы имеют фиксированный размер

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

array    // тип: массив, содержащий 42 целых числа

array // тип: массив, содержащий 10 строк

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

array::size_type i; // тип массива включает тип элемента

                             // и размер

array::size_type j;     // ошибка: array - это не тип

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

Фиксированный характер размера массивов влияет также на поведение остальных конструкторов, действительно определяющих массив. В отличие от других контейнеров, созданный по умолчанию массив не пуст: количество его элементов соответствует размеру, а инициализированы они значением по умолчанию (см. раздел 2.2.1), как и элементы встроенного массива (см. раздел 3.5.1). При списочной инициализации массива количество инициализаторов не должно превышать размер массива. Если инициализаторов меньше, чем элементов массива, они используются для первых элементов, а все остальные инициализируются значением по умолчанию (см. раздел 3.3.1). В любом случае, если типом элемента является класс, то у него должен быть стандартный конструктор, обеспечивающий инициализацию значением по умолчанию:

array ia1;        // десять целых чисел, инициализированных

                           // значением по умолчанию

array ia2 = {0,1,2,3,4,5,6,7,8,9}; // списочная инициализация

array ia3 = {42}; // ia3[0] содержит значение 42, остальные

                           // элементы - значение 0

Следует заметить, что хоть и нельзя копировать или присваивать объекты массивов встроенных типов (см. раздел 3.5.1), для контейнеров array такого ограничения нет:

int digs[10] = {0,1,2,3,4,5,6,7,8,9};

int cpy[10] = digs; // ошибка: встроенные массивы не допускают

                    // копирования и присвоения

array digits = {0,1,2,3,4,5,6,7,8,9};

array copy = digits; // ok: если типы массивов совпадают

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

Упражнения раздела 9.2.4

Упражнение 9.11. Приведите пример каждого из шести способов создания и инициализации контейнеров vector. Объясните, какие значения будет содержать каждый вектор.

Упражнение 9.12. Объясните различие между конструктором, получающим контейнер для копирования, и конструктором получающим два итератора.

Упражнение 9.13. Как инициализировать контейнер vector из контейнера list и контейнера vector? Напишите код для проверки ответов.

 

9.2.5. Присвоение и функция

swap()

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

c1 = c2;      // заменяет содержимое контейнера c1 копией

              // элементов контейнера c2

c1 = {a,b,c}; // после присвоения контейнер c1 имеет размер 3

После первого присвоения контейнеры слева и справа равны. Если контейнеры имели неравный размер, после присвоения у обоих будет размер контейнера из правого операнда. После второго присвоения размер контейнера c1 составит 3, что соответствует количеству значений, представленных в списке.

Таблица 9.4. Операторы присвоения контейнеров

c1 = c2 Заменяет элементы контейнера c1 копиями элементов контейнера c2 . Контейнеры c1 и c2 должны иметь тот же тип
с = {a, b, с...} Заменяет элементы контейнера c1 копиями элементов списка инициализации. (Недопустимо для массива.)
swap(c1, c2) c1.swap(c2) Обменивает элементы контейнеров c1 и c2 . Контейнеры c1 и c2 должны иметь тот же тип. Обычно функция swap() выполняется намного быстрее, чем процесс копирования элементов из контейнера c2 в c1
Операторы присвоения недопустимы для ассоциативных контейнеров и массива
seq.assign(b,е) Заменяет элементы в контейнере seq таковыми из диапазона, обозначенного итераторами b и е . Итераторы b и е не должны ссылаться на элементы в контейнере seq
seq.assign(il) Заменяет элементы в контейнере seq таковыми из списка инициализации il
seq.assign(n,t) Заменяет элементы в контейнере seq набором из n элементов со значением t

В отличие от встроенных массивов, библиотечный тип array поддерживает присвоение. У левых и правых операндов должен быть одинаковый тип:

array a1 = {0,1,2,3,4,5,6,7,8,9};

array а2 = {0}; // все элементы со значением 0

a1 = а2;  // замена элементов в a1

а2 = {0}; // ошибка: нельзя присвоить массиву значения из списка

Поскольку размер правого операнда может отличаться от размера левого операнда, тип array не поддерживает функцию assign() и это не позволяет присваивать значения из списка.

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

Применение функции assign() (только последовательные контейнеры)

Оператор присвоения требует совпадения типов левых и правых операндов. Он копирует все элементы правого операнда в левый. Последовательные контейнеры (кроме array) определяют также функцию-член assign(), обеспечивающую присвоение разных, но совместимых типов, или присвоение контейнерам последовательностей. Функция assign() заменяет все элементы в левом контейнере копиями элементов, указанных в ее аргументе. Например, функцию assign() можно использовать для присвоения диапазона значений char* из вектора в список строк:

list names;

vector oldstyle;

names = oldstyle; // ошибка: типы контейнеров не совпадают

// ok: преобразование из const char* в string возможно

names.assign(oldstyle.cbegin(), oldstyle.cend());

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

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

Вторая версия функции assign() получает целочисленное значение и значение элемента. Она заменяет указанное количество элементов контейнера заданным значением:

// эквивалент slist1.clear();

// сопровождается slist1.insert(slist1.begin(), 10, "Hiya!");

list slist1(1);     // один элемент; пустая строка

slist1.assign(10, "Hiya!"); // десять элементов; со значением "Hiya!"

Применение функции swap()

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

vector svec1(10); // вектор из 10 элементов

vector svec2(24); // вектор из 24 элементов

svec1.swap(svec2);

После выполнения функции swap() вектор svec1 содержит 24 строки, а вектор svec2 — 10. За исключением массивов смена двух контейнеров осуществляется очень быстро — сами элементы не меняются; меняются лишь внутренние структуры данных.

За исключением массивов функция swap() не копирует, не удаляет и не вставляет элементы, поэтому она гарантированно выполняется за постоянное время.

Благодаря тому факту, что элементы не перемещаются, итераторы, ссылки и указатели в контейнере, за исключением контейнера string, остаются допустимыми. Они продолжают указывать на те же элементы, что и перед перестановкой. Однако после вызова функции swap() эти элементы находятся в другом контейнере. Предположим, например, что итератор iter обозначал в векторе строк позицию svec1[3]Согласно другой трактовке, исключение — это объект системного или пользовательского класса, создаваемого операционной системой или кодом программы в ответ на обстоятельства, либо не допускающие дальнейшего нормального выполнения программы, либо определенные пользователем. Обработка исключений в приложении позволяет корректно выйти из затруднительной ситуации. — Примеч. ред .
. После вызова функции swap() он обозначит элемент в позиции svec2[3]Согласно другой трактовке, исключение — это объект системного или пользовательского класса, создаваемого операционной системой или кодом программы в ответ на обстоятельства, либо не допускающие дальнейшего нормального выполнения программы, либо определенные пользователем. Обработка исключений в приложении позволяет корректно выйти из затруднительной ситуации. — Примеч. ред .
. В отличие от других контейнеров, вызов функции swap() для строки делает некорректными итераторы, ссылки и указатели.

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

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

Новая библиотека предоставляет функцию swap() в версии члена класса контейнера и в версии, не являющейся членом какого-либо класса. Прежние версии библиотеки определили только версию функции-члена swap(). Функция swap() не являющаяся членом класса контейнера, имеет большое значение в обобщенных программах. Как правило, лучше использовать именно эту версию.

Упражнения раздела 9.2.5

Упражнение 9.14. Напишите программу, присваивающую значения элементов списка указателей на символьные строки в стиле С (тип char*) элементам вектора строк.

 

9.2.6. Операции с размером контейнера

За одним исключением у классов контейнеров есть три функции, связанные с размером. Функция-член size() (см. раздел 3.2.2) возвращает количество элементов в контейнере; функция-член empty() возвращает логическое значение true, если контейнер пуст, и значение false в противном случае; функция-член max_size() возвращает число, большее или равное количеству элементов, которые может содержать контейнер данного типа. По причинам, рассматриваемым в следующем разделе, контейнер forward_list предоставляет функции max_size() и empty(), но не функцию size().

 

9.2.7. Операторы сравнения

Для сравнения используется тот же реляционный оператор, который определен для типа элементов: при сравнении двух контейнеров на неравенство (!=) используется оператор != типа их элементов. Если тип элемента не поддерживает определенный оператор, то для сравнения контейнеров такого типа данный оператор использовать нельзя.

Сравнение двух контейнеров осуществляется на основании сравнения пар их элементов. Эти операторы работают так же, как и таковые у класса string (см. раздел 3.2.2):

• Если оба контейнера имеют одинаковый размер и все их элементы совпадают, контейнеры равны, в противном случае — не равны.

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

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

Проще всего понять работу операторов, рассмотрев их на примерах.

vector v1 = { 1, 3, 5, 7, 9, 12 };

vector v2 = { 1, 3, 9 };

vector v3 = { 1, 3, 5, 7 };

vector v4 = { 1, 3, 5, 7, 9, 12 };

v1 < v2  // true; v1 и v2 отличаются элементом [2]А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
: v1[2]А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
меньше,

         // чем v2[2]А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .

v1 < v3  // false; все элементы равны, но у v3 их меньше;

v1 == v4 // true; все элементы равны и размер v1 и v4 одинаков

v1 == v2 // false; v2 имеет меньше элементов, чем v1

При сравнении контейнеров используются операторы сравнения их элементов

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

Операторы равенства контейнеров используют оператор == элемента, а операторы сравнения — оператор < элемента. Если тип элемента не предоставляет необходимый оператор, то не получится использовать соответствующие операторы и с содержащими их контейнерами. Например, определенный в главе 7 тип Sales_data не предоставлял операторов == и <. Поэтому нельзя сравнить два контейнера, содержащих элементы типа Sales_data:

vector storeA, storeB;

if (storeA < storeB) // ошибка: Sales_data не имеет оператора <

Упражнения раздела 9.2.7

Упражнение 9.15. Напишите программу, выясняющую, равны ли два вектора vector.

Упражнение 9.16. Перепишите предыдущую программу, но сравните элементы списка list и вектора vector.

Упражнение 9.17. Допустим, c1 и c2 являются контейнерами. Какие условия налагают типы их элементов в следующем выражении?

if (c1 < c2)

 

9.3. Операции с последовательными контейнерами

 

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

 

9.3.1. Добавление элементов в последовательный контейнер

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

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

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

Таблица 9.5. Функции, добавляющие элементы в последовательный контейнер

Эти функции изменяют размер контейнера; они не поддерживаются массивами. Контейнер forward_list обладает специальными версиями функций insert() и emplace() ; см. раздел 9.3.4, а функций push_back() и emplace_back() у него нет. Функции push_front() и emplace_front() недопустимы для контейнеров vector и string .
c.push_back(t) c.emplace_back( args ) Создает в конце контейнера с элемент со значением t или переданным аргументом args . Возвращает тип void
c.push_front(t) c.emplace_front( args ) Создает в начале контейнера с элемент со значением t или переданным аргументом args . Возвращает тип void
c.insert(p,t) c.emplace(p,  args ) Создает элемент со значением t или переданным аргументом args перед элементом, обозначенным итератором p . Возвращает итератор на добавленный элемент
c.insert(p,n,t) Вставляет n элементов со значением t перед элементом, обозначенным итератором p . Возвращает итератор на первый вставленный элемент; если n — нуль, возвращается итератор p
c.insert(p,b,e) Вставляет элементы из диапазона, обозначенного итераторами b и е перед элементом, обозначенным итератором p . Итераторы b и е не могут относиться к элементам в контейнере с. Возвращает итератор на первый вставленный элемент; если диапазон пуст, возвращается итератор p
c.insert(p,il) il — это список значений элементов. Вставляет переданные значения перед элементом, обозначенным итератором p . Возвращает итератор на первый вставленный элемент; если список пуст, возвращается итератор p

Применение функции push_back()

В разделе 3.3.2 упоминалось, что функция push_back() добавляет элемент в конец вектора. Кроме контейнеров array и forward_list, каждый последовательный контейнер (включая string) поддерживает функцию push_back().

Цикл следующего примера читает по одной строке за раз в переменную word:

// читать слова со стандартного устройства ввода и помещать

// их в конец контейнера

string word;

while (cin >> word)

 container.push_back(word);

Вызов функции push_back() создает новый элемент в конце контейнера container, увеличивая его размер на 1. Значением этого элемента будет копия значения переменной word. Контейнер может иметь любой тип: list, vector или deque.

Поскольку класс string — это только контейнер символов, функцию push_back() можно использовать для добавления символов в ее конец:

void pluralize(size_t cnt, string &word) {

 if (cnt > 1)

  word.push_back('s'); // то же, что и word += 's'

}

Ключевая концепция. Элементы контейнера содержат копии значений

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

Применение функции push_front()

Кроме функции push_back(), контейнеры list, forward_list и deque предоставляют аналогичную функцию push_front(). Она вставляет новый элемент в начало контейнера:

list ilist;

// добавить элементы в начало ilist

for (size_t ix = 0; ix != 4; ++ix)

 ilist.push_front(ix);

Этот цикл добавляет элементы 0, 1, 2, 3 в начало списка ilist. Каждый элемент вставляется в новое начало списка, т.е. когда добавляется 1, она оказывается перед 0, 2 — перед 1 и так далее. Таким образом, добавленные в цикле элементы расположены в обратном порядке. После этого цикла список ilist содержит такую последовательность: 3, 2, 1, 0.

Обратите внимание: контейнер deque, предоставляющий подобно контейнеру vector быстрый произвольный доступ к своим элементам, обладает функцией-членом push_front(), а контейнер vector — нет. Контейнер deque гарантирует постоянное время вставки и удаления элементов в начало и в конец контейнера. Как и у контейнера vector, вставка элементов иначе как в начало или в конец контейнера deque — потенциально продолжительная операция.

Добавление элементов в контейнеры vector, string или deque способно сделать недействительными все существующие итераторы, ссылки и указатели на их элементы.

Добавление элементов в указанную точку контейнера

Функции push_back() и push_front() предоставляют весьма удобный способ добавления одиночных элементов в конец или в начало последовательного контейнера. Функция insert() обладает более общим характером и позволяет вставлять любое количество элементов в любую указанную позицию контейнера. Ее поддерживают контейнеры vector, deque, list и string, а контейнер forward_list предоставляет собственные специализированные версии этих функций-членов, которые рассматриваются в разделе 9.3.4.

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

slist.insert(iter, "Hello!"); // вставить "Hello!" прямо перед iter

Он вставляет строку со значением "Hello" непосредственно перед элементом, обозначенным итератором iter.

Даже при том, что у некоторых контейнеров нет функции push_front(), к функции insert() это не относится. Функция insert() позволяет вставлять элементы в начало контейнера, не заботясь о наличии у контейнера функции push_front():

vector svec;

list slist;

// эквивалент вызова slist.push_front("Hello!");

slist.insert(slist.begin(), "Hello!");

// вектор не имеет функции push_front(),

//  но вставка перед begin() возможна

// внимание: вставка возможна везде, но вставка в конец вектора может

// потребовать больше времени

svec.insert(svec.begin(), "Hello!");

Контейнеры vector, deque и string допускают вставку в любую позицию, но это может потребовать больше времени.

Вставка диапазона элементов

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

svec.insert(svec.end(), 10, "Anna");

Этот код вставляет 10 элементов в конец вектора svec и инициализирует каждый из них строкой "Anna".

Данная версия функции insert() получает пару итераторов или список инициализации для вставки элементов из данного диапазона перед указанной позицией:

vector v = {"quasi", "simba", "frollo", "scar"};

// вставить два последних элемента вектора v в начало slist

slist.insert(slist.begin(), v.end() - 2, v.end());

slist.insert(slist.end(), {"these", "words", "will",

                           "go", "at", "the", "end"});

// ошибка времени выполнения:

//  обозначающие копируемый диапазон итераторы

// не должны принадлежать тому же контейнеру, который изменяется

slist.insert(slist.begin(), slist.begin(), slist.end());

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

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

Применение возвращаемого значения функции insert()

Значение, возвращенное функцией insert(), можно использовать для многократной вставки элементов в определенной позиции контейнера:

list lst;

auto iter = lst.begin();

while (cin >> word)

 iter = lst.insert(iter, word); // то же, что и вызов push_front()

Важно понимать, почему именно цикл, подобный этому, эквивалентен вызову функции push_front().

Перед циклом итератор iter инициализируется возвращаемым значением функции lst.begin(). Первый вызов функции insert() получает только что прочитанную строку и помещает ее перед элементом, обозначенным итератором iter. Значение, возвращенное функцией insert(), является итератором на этот новый элемент. Присвоим этот итератор итератору iter и повторим цикл, читая другое слово. Пока есть слова для вставки, каждая итерация цикла while вставляет новый элемент перед позицией iter и снова присваивает ему позицию недавно вставленного элемента. Этот (новый) элемент является первым. Таким образом, каждая итерация вставляет элемент перед первым элементом в списке.

Применение функций emplace()

Новый стандарт вводит три новых функции-члена — emplace_front(), emplace() и emplace_back(), которые создают элементы, а не копируют. Они соответствуют функциям push_front(), insert() и push_back(), позволяющим помещать элемент в начало контейнера, перед указанной позицией или в конец контейнера соответственно.

Когда происходит вызов функции-члена insert() или push(), им передается объект типа элемента для копирования в контейнер. Когда происходит вызов функции emplace(), ее аргументы передаются конструктору типа элемента, который создает элемент непосредственно в области, контролируемой контейнером. Предположим, например, что контейнер с содержит элементы типа Sales_data (см. раздел 7.1.4):

// создает объект класса Sales_data в конце контейнера с

// использует конструктор класса Sales_data с тремя аргументами

с.emplace_back("978-05903534 03", 25, 15.99);

// ошибка: нет версии функции push_back(), получающей три аргумента

с.push_back("978-0590353403", 25, 15.99);

// ok: создается временный объект класса Sales_data для передачи

// функции push_back()

c.push_back(Sales_data("978-0590353403", 25, 15.99));

Вызов функции emplace_back() и второй вызов функции push_back() создают новые объекты класса Sales_data. При вызове функции emplace_back() этот объект создается непосредственно в области, контролируемой контейнером. Вызов функции push_back() создает локальный временный объект, который помещается в контейнер.

Аргументы функции emplace() зависят от типа элемента, они должны соответствовать конструктору типа элемента:

// iter указывает на элемент класса Sales_data контейнера с

с.emplace_back(); // использует стандартный конструктор

                  //  класса Sales_data

с.emplace(iter, "999-999999999"); // используется Sales_data(string)

// использует конструктор класса Sales_data, получающий ISBN,

//  количество и цену

с.emplace_front("978-0590353403", 25, 15.99);

Функция emplace() создает элементы контейнера. Ее аргументы должны соответствовать конструктору типа элемента.

Упражнения раздела 9.3.1

Упражнение 9.18. Напишите программу чтения последовательности строк со стандартного устройства ввода в контейнер deque. Для записи элементов в контейнер deque используйте итераторы и цикл.

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

Упражнение 9.20. Напишите программу, копирующую элементы списка list в две двухсторонние очереди, причем нечетные элементы должны копироваться в один контейнер deque, а четные в другой.

Упражнение 9.21. Объясните, как цикл из пункта «Применение возвращаемого значения функции insert()», использующий возвращаемое значение функции insert() и добавляющий элементы в список, работал бы с вектором вместо списка.

Упражнение 9.22. С учетом того, что iv является вектором целых чисел, что не так со следующей программой? Как ее можно исправить?

vector::iterator iter = iv.begin(),

mid = iv.begin() + iv.size()/2;

while (iter != mid)

 if (*iter == some_val)

  iv.insert(iter, 2 * some_val);

 

9.3.2. Доступ к элементам

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

У каждого последовательного контейнера, включая array, есть функция-член front(), и у всех, кроме forward_list, есть также функция-член back(). Эти функции возвращают ссылку на первый и последний элементы соответственно:

// перед обращением к значению итератора удостовериться, что

// элемент существует, либо вызвать функции front() и back()

if (!с.empty()) {

 // val и val2 - копии значений первого элемента в контейнере с

 auto val = *с.begin(), val2 = c.front();

 // val3 и val4 - копии значений последнего элемента в контейнере с

 auto last = c.end();

 auto val3 = *(--last); // невозможен декремент итератора forward_list

 auto val4 = c.back();  // не поддерживается forward_list

}

Таблица 9.6. Функции доступа к элементам последовательного контейнера

Функция at() и оператор индексирования допустимы только для контейнеров string , vector , deque и array . Функция back() недопустима для контейнера forward_list .
c.back() Возвращает ссылку на последний элемент контейнера с , если он не пуст
c.front() Возвращает ссылку на первый элемент контейнера с , если он не пуст
c[n] Возвращает ссылку на элемент, индексированный целочисленным беззнаковым значением n . Если n >= c.size() , результат непредсказуем
c.at(n) Возвращает ссылку на элемент по индексу n . Если индекс находится вне диапазона, передает исключение out_of_range

Эта программа получает ссылки на первый и последний элементы контейнера с двумя разными способами. Прямой подход — обращение к функциям front() и back(). Косвенный подход получения ссылки на тот же элемент подразумевает обращение к значению итератора, возвращенного функцией begin(), или декремент с последующим обращением к значению итератора, возвращенного функцией end().

В этой программе примечательны два момента: поскольку возвращенный функцией end() итератор указывает на элемент, следующий после последнего, для доступа к последнему элементу контейнера применяется декремент полученного итератора. Вторым очень важным моментом является необходимость удостовериться в том, что контейнер c не пуст, перед вызовом функций front() и back() или обращением к значению итераторов, возвращенных функциями begin() и end(). Если контейнер окажется пустым, все выражения в блоке операторов if будут некорректны.

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

Функции-члены, обращающиеся к элементам в контейнере (т.е. функции front(), back(), at() и индексирование), возвращают ссылки. Если контейнер является константным объектом, возвращается ссылка на константу. Если контейнер не константа, возвращается обычная ссылка, которую можно использовать для изменения значения выбранного элемента:

if (!с.empty()) {

 с.front() = 42;     // присвоить 42 первому элементу в контейнере с

 auto &v = c.back(); // получить ссылку на последний элемент

 v = 1024;           // изменить элемент в контейнере с

 auto v2 = c.back(); // v2 не ссылка; это копия c.back()

 v2 = 0;             // это не изменит элемент в контейнере с

}

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

Индексация и безопасный произвольный доступ

Контейнеры, предоставляющие быстрый произвольный доступ (string, vector, deque и array), предоставляют также оператор индексирования (см. раздел 3.3.3). Как уже упоминалось, оператор индексирования получает индекс и возвращает ссылку на элемент в этой позиции контейнера. Индекс не должен выходить за диапазон элементов (т.е. больше или равен 0 и меньше размера контейнера). Допустимость индекса должен обеспечить разработчик; оператор индексирования не проверяет принадлежность индекса диапазону. Использование для индекса значения вне диапазона является серьезной ошибкой, но компилятор ее не обнаружит.

Если необходимо гарантировать допустимость индекса, вместо него можно использовать функцию-член at(). Она действует как оператор индексирования, но если индекс недопустим, то она передает исключение out_of_range (см. раздел 5.6):

vector svec; // пустой вектор

cout << svec[0];     // ошибка времени выполнения: вектор svec

                     // еще не имеет элементов!

cout << svec.at(0);  // передает исключение out_of_range

Упражнения раздела 9.3.2

Упражнение 9.23. Какими были бы значения переменных val2, val3 и val4, в первой программе данного раздела, если бы функция c.size() возвращала значение 1?

Упражнение 9.24. Напишите программу, которая обращается к первому элементу вектора, используя функции at(), front() и begin(), а также оператор индексирования. Проверьте программу на пустом векторе.

 

9.3.3. Удаление элементов

Подобно тому, как существует несколько способов добавления элементов в контейнер (исключая array), существует несколько способов их удаления. Функции удаления перечислены в табл. 9.7.

Таблица 9.7. Функции удаления последовательных контейнеров

Эти функции изменяют размер контейнера; они не поддерживаются массивами. Контейнер forward_list обладает специальной версией функции erase() ; см. раздел 9.3.4, а функции pop_back() у него нет. Функция pop_front() недопустима для контейнеров vector и string .
c.pop_back() Удаляет последний элемент контейнера c . Результат непредсказуем, если контейнер c пуст. Возвращает void
c.pop_front() Удаляет первый элемент контейнера с . Результат непредсказуем, если контейнер с пуст. Возвращает void
c.erase(p) Удаляет элемент, обозначенный итератором p . Возвращает итератор на элемент после удаленного или итератор после конца (off-the-end iterator), если итератор p обозначает последний элемент. Результат непредсказуем, если итератор p указывает на следующий элемент после последнего
c.erase(b,е) Удаляет диапазон элементов, обозначенных итераторами b и е . Возвращает итератор на элемент после последнего удаленного или после последнего элемента контейнера, если итератор е указывал на последний элемент
c.clear() Удаляет все элементы контейнера с . Возвращает void

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

Применение функций pop_front() и pop_back()

Функции pop_front() и pop_back() удаляют, соответственно, первый и последний элементы контейнера. Векторы и строки функциями push_front() и pop_front() не обладают. У контейнера forward_list также нет функции pop_back(). Подобно функциям-членам доступа к элементам, эти функции не применимы к пустому контейнеру.

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

while (!ilist.empty()) {

 process(ilist.front());  // действия с текущей вершиной списка ilist

 ilist.pop_front(); // готово; удалить первый элемент

}

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

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

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

В качестве примера рассмотрим следующий цикл, удаляющий нечетные элементы списка:

list lst = {0,1,2,3,4,5,6,7,8,9};

auto it = lst.begin();

while (it != lst.end())

 if (*it % 2)         // если элемент является нечетным

  it = lst.erase(it); // удалить его

 else

  ++it;

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

Удаление нескольких элементов

Версия функции erase() с парой итераторов позволяет удалить диапазон элементов:

// удалить диапазон элементов между двумя итераторами

// возвращает итератор на элемент сразу после последнего удаленного

elem1 = slist.erase(elem1, elem2); // после вызова elem1 == elem2

Итератор elem1 указывает на первый удаляемый элемент, а итератор elem2 — на следующий после последнего удаляемого.

Чтобы удалить все элементы в контейнере, можно либо вызвать функцию clear(), либо перебирать итераторы от возвращаемого функцией begin() до end() и передавать их функции erase():

slist.clear(); // удалить все элементы в контейнере

slist.erase(slist.begin(), slist.end()); // эквивалент

Упражнения раздела 9.3.3

Упражнение 9.25. Что будет, если в программе, где удалялся диапазон элементов, итераторы elem1 и elem2 равны? Что если итератор elem2 или оба итератора (elem1 и elem2) являются итератором после конца?

Упражнение 9.26. Используя приведенное ниже определение массива ia, скопируйте его содержимое в вектор и в список. Используя версию функции erase() для одного итератора, удалите из списка элементы с нечетными значениями, а из вектора — с четными.

int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };

 

9.3.4. Специализированные функции контейнера

forward_list

Чтобы лучше понять, почему у контейнера forward_list есть специальные версии функций добавления и удаления элементов, рассмотрим, что происходит при удалении элемента из односвязного списка. Как показано на рис. 9.1, удаление элемента изменяет связи в последовательности. В данном случае удаление элемента elem3 изменяет связь элемента elem2; элемент elem2 указывал на элемент elem3, но после удаления элемента elem3 элемент elem2 указывает на элемент elem4.

Рис. 9.1. Специализированные функции контейнера forward_list

При добавлении или удалении элемента у элемента перед ним будет другой последователь. Чтобы добавить или удалить элемент, необходимо обратиться к его предшественнику и изменить его ссылку. Однако контейнер forward_list — это односвязный список. В односвязном списке нет простого способа доступа к предыдущему элементу. Поэтому функции добавления и удаления элементов в контейнере forward_list работают, изменяя элемент после указанного. Таким образом, у нас всегда есть доступ к элементам, на которые влияет изменение.

Поскольку эти функции ведут себя отлично от функций других контейнеров, класс forward_list не определяет функции-члены insert(), emplace() и erase(). Вместо них используются функции-члены insert_after(), emplace_after() и erase_after() (перечислены в табл. 9.8). На рисунке выше, например, для удаления элемента elem3 использовалась бы функция erase_after() для итератора, обозначающего элемент elem2. Для реализации этих операций класс forward_list определяет также функцию before_begin(), которая возвращает итератор после начала (off-the-beginning iterator). Этот итератор позволяет добавлять или удалять элементы после несуществующего элемента перед первым в списке.

Таблица 9.8. Функции вставки и удаления элементов контейнера forward_list

lst.before_begin() lst.cbefore_begin() Возвращает итератор, обозначающий несуществующий элемент непосредственно перед началом списка. К значению этого итератора обратиться нельзя. Функция cbefore_begin() возвращает итератор const_iterator
lst.insert_after(p,t) lst.insert_after(p,n,t) lst.insert_after(p,b,e) lst.insert_after(p,il) Вставляет элемент (элементы) после обозначенного итератором p . t — это объект, n — количество, b и е — обозначающие диапазон итераторы (они не должны принадлежать контейнеру lst ), il — список, заключенный в скобки. Возвращает итератор на последний вставленный элемент. Если диапазон пуст, возвращается итератор p . Если p — итератор после конца, результат будет непредсказуемым
emplace_after(p, args ) Параметр args используется для создания элементов после обозначенного итератором p . Возвращает итератор на новый элемент. Если p — итератор после конца, результат будет непредсказуемым
lst.erase_after(p) lst.erase_after(b,e) Удаляет элемент после обозначенного итератором p или диапазоном элементов после обозначенного итератором b и до, но не включая обозначенного итератором е . Возвращает итератор на элемент после удаленного или на элемент после конца контейнера. Если p  — итератор после конца или последний элемент контейнера, результат будет непредсказуемым

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

forward_list flst = {0,1,2,3,4,5,6,7,8,9};

auto prev = flst.before_begin(); // обозначает элемент "перед началом"

                                 // контейнера flst

auto curr = flst.begin(); // обозначает первый элемент контейнера flst

while (curr != flst.end()) { // пока есть элементы для обработки

 if (*curr % 2)                  // если элемент нечетный

  curr = flst.erase_after(prev); // удалить его и переместить curr

 else {

  prev = curr; // переместить итератор на следующий элемент

  ++curr;      // и один перед следующим элементом

 }

}

Здесь итератор curr обозначает проверяемый элемент, а итератор prev — элемент перед curr. Итератор curr инициализирует вызов функции begin(), чтобы первая итерация проверила на четность первый элемент. Итератор prev инициализирует вызов функции before_begin(), который возвращает итератор на несуществующий элемент непосредственно перед curr.

Когда находится нечетный элемент, итератор prev передается функции erase_after(). Этот вызов удаляет элемент после обозначенного итератором prev; т.е. элемент, обозначенный итератором curr. Итератору curr присваивается значение, возвращенное функцией erase_after(). В результате он обозначит следующий элемент последовательности, а итератор prev останется неизменным; он все еще обозначает элемент перед (новым) значением итератора curr. Если обозначенный итератором curr элемент не является нечетным, то в части else оба итератора перемещаются на следующий элемент.

Упражнения раздела 9.3.4

Упражнение 9.27. Напишите программу для поиска и удаления нечетных элементов в контейнере forward_list.

Упражнение 9.28. Напишите функцию, получающую контейнер forward_list и два дополнительных аргумента типа string. Функция должна находить первую строку и вставлять вторую непосредственно после первой. Если первая строка не найдена, то вставьте вторую строку в конец списка.

 

9.3.5. Изменение размеров контейнера

Для изменения размера контейнера, за исключением массива, можно использовать функцию resize(), представленную в табл. 9.9. Если текущий размер больше затребованного, элементы удаляются с конца контейнера; если текущий размер меньше нового, элементы добавляются в конец контейнера:

list ilist(10, 42); // 10 целых чисел со значением 42

ilist.resize(15);        // добавляет 5 элементов со значением 0

                         // в конец списка ilist

ilist.resize(25, -1);    // добавляет 10 элементов со значением -1

                         // в конец списка ilist

ilist.resize(5);         // удаляет 20 элементов от конца списка ilist

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

Таблица 9.9. Функции размера последовательного контейнера

За исключением массива
c.resize(n) Измените размеры контейнера с так, чтобы у него было n элементов. Если n < c.size() , то лишние элементы отбрасываются. Если следует добавить новые элементы, они инициализируются значением по умолчанию
с.resize(n,t) Измените размеры контейнера с так, чтобы у него было n элементов. Все добавляемые элементы получат значение t

Если функция resize() сокращает контейнер, то итераторы, ссылки и указатели на удаленные элементы окажутся некорректными; выполнение функции resize() для контейнеров vector, string и deque может сделать некорректными все итераторы, указатели и ссылки.

Упражнения раздела 9.3.5

Упражнение 9.29. Если контейнер vec содержит 25 элементов, то что делает выражение vec.resize(100)? Что если затем последует вызов vec.resize(10)?

Упражнение 9.30. Какие ограничения (если они есть) налагает использование функции resize() с одиночным аргументом, имеющим тип элемента?

 

9.3.6. Некоторые операции с контейнерами делают итераторы недопустимыми

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

После операции добавления элементов в контейнер возможно следующее.

• Итераторы, указатели и ссылки на элементы вектора или строки становятся недопустимыми после повторного резервирования пространства контейнера. Если повторного резервирования не было, ссылки на элементы перед позицией вставки остаются допустимыми, а на элементы после позиции вставки — нет.

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

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

• У контейнеров list и forward_list все остальные итераторы, ссылки и указатели (включая итераторы после конца и перед началом) остаются допустимыми.

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

• У контейнеров vector и string все остальные итераторы, ссылки и указатели на элементы перед позицией удаления остаются допустимыми. При удалении элементов итератор после конца всегда оказывается недопустимым.

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

Совет. Контроль итераторов

При использовании итератора (или ссылки, или указателя на элемент контейнера) имеет смысл минимизировать ту часть программы, где итератор обязан оставаться допустимым.

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

#magnify.png Создание циклов, которые изменяют контейнер

Циклы, добавляющие или удаляющие элементы из контейнеров vector, string или deque, должны учитывать тот факт, что итераторы, ссылки и указатели могут стать недопустимыми. Программа должна гарантировать, что итератор, ссылка или указатель обновляется на каждом цикле. Если цикл использует функцию insert() или erase(), обновить итератор довольно просто. Они возвращают итераторы, которые можно использовать для переустановки итератора:

// бесполезный цикл, удаляющий четные элементы и вставляющий дубликаты

// нечетных

vector vi = {0,1,2,3,4,5,6,7,8,9};

auto iter = vi.begin(); // поскольку vi изменяется, используется

                        // функция begin(), а не cbegin()

while (iter != vi.end()) {

 if (*iter % 2) {

  iter = vi.insert(iter, *iter); // дублирует текущий элемент

  iter +=2;                      // переместить через элемент

 } else

  iter = vi.erase(iter); // удалить четные элементы

 // не перемещать итератор; iter обозначает элемент после

 // удаленного

}

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

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

#magnify.png Не храните итератор, возвращенный функцией end()

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

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

// ошибка: поведение этого цикла непредсказуемо

auto begin = v.begin(),

 end = v.end(); // плохая идея хранить значение итератора end

while (begin != end) {

 // некоторые действия

 // вставить новое значение и переприсвоить итератор begin, который

 // в противном случае окажется недопустимым

 ++begin; // переместить begin, поскольку вставка необходима после

          // этого элемента

 begin = v.insert(begin, 42); // вставить новое значение

 ++begin; // переместить begin за только что добавленный элемент

}

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

Не кешируйте возвращаемый функцией end() итератор в циклах, которые вставляют или удаляют элементы в контейнере deque, string или vector.

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

// существенно безопасный: повторно вычислять end после каждого

// добавления/удаления элементов

while (begin != v.end()) {

 // некоторые действия

 ++begin; // переместить begin, поскольку вставка необходима после

          // этого элемента

 begin = v.insert(begin, 42); // вставить новое значение

 ++begin; // переместить begin за только что добавленный элемент

}

Упражнения раздела 9.3.6

Упражнение 9.31. Программа из пункта «Создание циклов, которые изменяют контейнер», удаляющая четные и дублирующая нечетные элементы, не будет работать с контейнером list или forward_list. Почему? Переделайте программу так, чтобы она работала и с этими типами тоже.

Упражнение 9.32. Будет ли допустим в указанной выше программе следующий вызов функции insert()? Если нет, то почему?

iter = vi.insert(iter, *iter++);

Упражнение 9.33. Что будет, если в последнем примере этого раздела не присваивать переменной begin результат вызова функции insert()? Напишите программу без этого присвоения, чтобы убедиться в правильности своего предположения.

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

iter = vi.begin();

while (iter != vi.end())

 if (*iter % 2)

  iter = vi.insert(iter, *iter);

 ++iter;

 

9.4. Как увеличивается размер вектора

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

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

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

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

Функции-члены управления емкостью

Типы vector и string предоставляют описанные в табл. 9.10 функции-члены, позволяющие взаимодействовать с частью реализации, относящейся к резервированию памяти. Функция capacity() сообщает количество элементов, которое контейнер может создать прежде, чем ему понадобится занять больший объем памяти. Функция reserve() позволяет задать количество резервируемых элементов.

Таблица 9.10. Управление размером контейнера

Функция shrink_to_fit() допустима только для контейнеров vector , string и deque . Функции capacity() и reserve() допустимы только для контейнеров vector и string .
c.shrink_to_fit() Запрос на уменьшение емкости в соответствии с размером
c.capacity() Количество элементов, которое может иметь контейнер с прежде, чем понадобится повторное резервирование
c.reserve(n) Резервирование места по крайней мере для n элементов

Функция reserve() не изменяет количество элементов в контейнере; она влияет только на объем памяти, предварительно резервируемой вектором.

Вызов функции reserve() изменяет емкость вектора, только если требуемое пространство превышает текущую емкость. Если требуемый размер больше текущей емкости, функция reserve() резервирует по крайней мере столько места, сколько затребовано (или несколько больше).

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

В результате вызов функции reserve() никогда не сократит объем контейнера. Точно так же функция-член resize() (см. раздел 9.3.5) изменяет только количество элементов контейнера, а не его емкость. Функцию resize() нельзя использовать для сокращения объема память, которую контейнер держит в резерве.

В новой библиотеке есть функция shrink_to_fit(), позволяющая запросить контейнеры deque, vector или string освободить неиспользуемую память. Вызов этой функции означает, что никакой резервной емкости больше не нужно. Однако реализация имеет право проигнорировать этот запрос. Нет никакой гарантии, что вызов функции shrink_to_fit() освободит память.

Функции-члены capacity() и size()

Очень важно понимать различие между емкостью (capacity) и размером (size). Размер — это количество элементов, хранящихся в контейнере в настоящий момент, а емкость — это количество элементов, которое контейнер может содержать, не прибегая к следующей операции резервирования памяти. Следующий код иллюстрирует взаимосвязь размера и емкости:

vector ivec;

// размер нулевой; емкость зависит от реализации

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

// присвоить вектору ivec 24 элемента

for (vector::size_type ix = 0; ix != 24; ++ix)

 ivec.push_back(ix);

// размер 24; емкость равна или больше 24, согласно реализации

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

При запуске на компьютере автора эта программа отобразила следующий результат:

ivec: size: 0 capacity: 0

ivec: size: 24 capacity: 32

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

Визуально текущее состояние вектора ivec можно представить так:

Теперь при помощи функции reserve() можно зарезервировать дополнительное пространство.

ivec.reserve(50); // задать емкость 50 элементов (можно и больше)

// размер будет 24, а емкость - 50 или больше, если так определено

// в реализации

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

Вывод свидетельствует о том, что вызов функции reserve() зарезервировал именно столько места, сколько было запрошено:

ivec: size: 24 capacity: 50

Эту резервную емкость можно впоследствии израсходовать следующим образом:

// добавить элементы, чтобы исчерпать резервную емкость

while (ivec.size() != ivec.capacity())

 ivec.push_back(0);

// емкость не изменилась, размер и емкость теперь равны

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

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

ivec: size: 50 capacity: 50

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

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

ivec.push_back(42); // добавить еще один элемент

// размер будет 51, а емкость 51 или больше, если так определено

// в реализации

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

Результат выполнения этой части программы имеет следующий вид:

ivec: size: 51 capacity: 100

Он свидетельствует о том, что в данной реализации класса vector использована стратегия удвоения текущей емкости при каждом резервировании новой области памяти.

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

ivec.shrink_to_fit(); // запрос на возвращение памяти

// размер остался неизменным; емкость определена реализацией

cout << "ivec: size: " << ivec.size()

     << " capacity: " << ivec.capacity() << endl;

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

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

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

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

Упражнения раздела 9.4

Упражнение 9.35. Объясните различие между емкостью вектора и его размером.

Упражнение 9.36. Может ли контейнер иметь емкость, меньшую, чем его размер?

Упражнение 9.37. Почему контейнеры list и array не имеют функции-члена capacity()?

Упражнение 9.38. Напишите программу, позволяющую исследовать рост вектора в библиотеке, которую вы используете.

Упражнение 9.39. Объясните, что выполняет следующий фрагмент программы:

vector svec;

svec.reserve(1024);

string word;

while (cin >> word)

 svec.push_back(word);

svec.resize(svec.size() + svec.size()/2);

Упражнение 9.40. Если программа в предыдущем упражнении читает 256 слов, какова ее вероятная емкость после вызова функции resize()? Что, если она читает 512, 1 000 или 1 048 слов?

 

9.5. Дополнительные операции со строками

 

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

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

 

9.5.1. Дополнительные способы создания строк

В дополнение к конструкторам, описанным в разделе 3.2.1, и конструкторам, общим для всех последовательных контейнеров (см. табл. 9.3), тип string предоставляет еще три конструктора, описанные в табл. 9.11.

Таблица 9.11. Дополнительные способы создания строк 

Все значения n , len2 и pos2 являются беззнаковыми.
string s(cp, n); Строка s — копия первых n символов из массива, на который указывает cp . У того массива должно быть по крайней мере n символов
string s(s2, pos2); Строка s — копия символов из строки s2 , начиная с позиции по индексу pos2 . Если pos2 > s2.size() , результат непредсказуем
string s(s2, pos2, len2); Строка s — копия len2 символов из строки s2 , начиная с позиции по индексу pos2 . Если pos2 > s2.size() , то результат непредсказуем. Независимо от значения len2 , копируется по крайней мере s2.size() - pos2 символов

Конструкторы, получающие тип string или const char*, получают дополнительные (необязательные) аргументы, позволяющие задать количество копируемых символов. При передаче строки можно также указать индекс начала копирования:

const char *cp = "Hello World!!!"; // массив с нулевым символом в конце

char noNull[] = {'H', 'i' };       // без нулевого символа в конце

string s1(cp); // копирует cp до нулевого символа;

               //  s1 == "Hello World!!!"

string s2(noNull,2);  // копирует два символа из noNull; s2 == "Hi"

string s3(noNull);    // непредсказуемо: noNull не завершается null

string s4(cp + 6, 5); // копирует 5 символов, начиная с cp[6];

                      // s4 == "World"

string s5(s1, 6, 5);  // копирует 5 символов, начиная с s1[6];

                      // s5 == "World"

string s6(s1, 6);     // копирует от s1[6] до конца s1;

                      //  s6 == "World!!!"

string s7(s1, 6, 20); // ok, копирует только до конца s1;

                      // s7 == "World!!!"

string s8(s1, 16);    // передает исключение out_of_range

Обычно строка создается из типа const char*. Массив, на который указывает указатель, должен завершаться нулевым символом; символы копируются до нулевого символа. Если передается также количество, массив не обязан заканчиваться нулевым символом. Если количество не указано и нет нулевого символа или если указанное количество больше размера массива, результат непредсказуем.

При копировании из строки можно предоставить необязательный параметр исходной позиции и количество копируемых символов. Исходная позиция должна быть меньше или равна размеру переданной строки. Если исходная позиция больше размера, то конструктор передаст исключение out_of_range (см. раздел 5.6). При передаче количества копирование начинается с указанной позиции. Независимо от количества запрошенных символов, копирование осуществляется до размера строки, но не более.

Функция substr()

Функция substr() (представленная в табл. 9.12) возвращает копию части или всей исходной строки. Ей можно передать (необязательно) начальную позицию и количество копируемых символов:

string s("hello world");

string s2 = s.substr(0, 5);  // s2 = hello

string s3 = s.substr(6);     // s3 = world

string s4 = s.substr(6, 11); // s3 = world

string s5 = s.substr(12);    // передает исключение out_of_range

Если начальная позиция превышает размер исходной строки, функция substr() передает исключение out_of_range (см. раздел 5.6). Если начальная позиция в сумме с количеством копируемых символов превосходит размер строки, то копирование осуществляется только до конца строки.

Таблица 9.12. Функция substr()

s.substr(pos, n) Возвращает строку, содержащую n символов из строки s , начиная с позиции pos . По умолчанию параметр pos имеет значение 0. Параметр n по умолчанию имеет значение, подразумевающее копирование всех символов строки s , начиная с позиции pos

Упражнения раздела 9.5.1

Упражнение 9.41. Напишите программу, инициализирующую строку из вектора vector.

Упражнение 9.42. Учитывая, что символы в строку следует читать по одному и заранее известно, что прочитать предстоит по крайней мере 100 символов, как можно было бы улучшить производительность программы?

 

9.5.2. Другие способы изменения строки

Тип string поддерживает операторы присвоения последовательного контейнера, а также функции assign(), insert() и erase() (см. раздел 9.2.5, раздел 9.3.1 и раздел 9.3.3). В нем также определены дополнительные версии функций insert() и erase().

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

s.insert(s.size(), 5, '!'); // вставить пять восклицательных знаков

                            // в конец строки s

s.erase(s.size() - 5, 5);   // удалить последние пять символов из

                            // строки s

Библиотека string предоставляет также версии функций insert() и assign(), получающие массивы символов в стиле С. Например, символьный массив с нулевым символом в конце можно использовать как значение, передаваемое функциям insert() и assign():

const char *cp = "Stately, plump Buck";

s.assign(cp, 7);            // s == "Stately"

s.insert(s.size(), cp + 7); // s == "Stately, plump Buck"

Сначала содержимое строки s заменяется при вызове функции assign(). Присваиваемые строке s символы — это семь символов из начала массива, на который указывает указатель cp. Количество запрашиваемых символов должно быть меньше или равно количеству символов массива (исключая завершающий нулевой символ), на который указывает cp.

Когда происходит вызов функции insert() для строки s, подразумевается вставка символов перед несуществующим элементом в позиции s[size()]. В данном случае копируются символы, начиная с седьмого символа cp и до завершающего нулевого символа.

Символы для функций insert() и assign() можно также указать как исходящие из другой строки или ее подстроки:

string s = "some string", s2 = "some other string";

s.insert(0, s2); // вставить копию s2 перед позицией 0 в s

// вставить s2.size() символов из s2,

//  начиная с позиции s2[0] перед s[0]

s.insert(0, s2, 0, s2.size());

Функции append() и replace()

Класс string определяет две дополнительные функции-члена, append() и replace(), способные изменить содержимое строки. Все эти функции описаны в табл. 9.13. Функция append() — упрощенный способ вставки в конец:

string s("C++ Primer"), s2 = s; // инициализация строк s и s2

                                // текстом "С++ Primer"

s.insert(s.size(), " 4th Ed."); // s == "С++ Primer 4th Ed."

s2.append(" 4th Ed."); // эквивалент: добавление " 4th Ed." к s2;

                       //  s == s2

Функция replace() — упрощенный способ вызова функций erase() и insert():

// эквивалентный способ замены "4th" на "5th"

s.erase(11, 3); // s == "С++ Primer Ed."

s.insert(11, "5th"); // s == "С++ Primer 5th Ed."

// начиная с позиции 11, удалить три символа, а затем вставить "5th"

s2.replace(11, 3, "5th"); // эквивалент: s == s2

В вызове функции replace() вставляемый текст может быть того же размера, что и удаляемый. Но можно вставить большую или меньшую строку:

s.replace(11, 3, "Fifth"); // s == "С++ Primer Fifth Ed."

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

Таблица 9.13. Функции изменения содержимого строки

s.insert( pos , args ) Вставка символов, определенных аргументом args , перед позицией pos . Позиция pos может быть задана индексом или итератором. Версии, получающие индекс, возвращают ссылку на строку s , а получающие итератор возвращают итератор, обозначающий первый вставленный символ
s.erase( pos , len ) Удаляет len символов, начиная с позиции pos . Если аргумент len пропущен, удаляет символы от позиции pos до конца строки s . Возвращает ссылку на строку s
s.assign( args ) Заменяет символы строки s согласно аргументу args . Возвращает ссылку на строку s
s.append( args ) Добавляет аргумент args к строке s . Возвращает ссылку на строку s
s.replace( range , args ) Удаляет диапазон range символов из строки s и заменяет их символами, заданными аргументом args . Диапазон задан либо индексом и длиной, либо парой итераторов. Возвращает ссылку на строку s
Аргументы args могут быть одним из следующих: функции append() и assign() могут использовать все формы. Строка str должна быть отлична от s , а итераторы b и е не могут принадлежать строке s
str Строка str
str , pos , len До len символов из строки str , начиная с позиции pos
cp , len До len символов из символьного массива, на который указывает указатель cp
cp Завершающийся нулевым символом массив, на который указывает указатель cp
n , c n копий символа с
b , e Символы в диапазоне, указанном итераторами b и е
Список инициализации Разделяемый запятыми список символов, заключенный в фигурные скобки
Аргументы args для функций replace() и insert() зависят от того, использован ли диапазон или позиция
replace(pos,len, args ) replace(b,е, args ) insert(pos, args ) insert(iter, args ) Возможные аргументы args
Да Да Да Нет str
Да Нет Да Нет str , pos , len
Да Да Да Нет cp , len
Да Да Нет Нет cp
Да Да Да Да n , с
Нет Да Нет Да b2 , e2
Нет Да Нет Да список инициализации

Множество способов изменить строку

Функции append(), assign(), insert() и replace(), перечисленные в табл. 9.13, имеют несколько перегруженных версий. Аргументы этих функций зависят от того, как заданы добавляемые символы и какая часть строки изменится. К счастью, у этих функций общий интерфейс.

У функций assign() и append() нет необходимости определять изменяемые части строки: функция assign() всегда заменяет все содержимое строки, а функция append() всегда добавляет в конец строки.

Функция replace() предоставляет два способа определения диапазона удаления символов. Диапазон можно определить по позиции и длине или парой итераторов. Функция insert() предоставляет два способа определения позиции вставки: при помощи индекса или итератора. В любом случае новый элемент (элементы) вставляется перед указанным индексом или итератором.

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

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

Упражнения раздела 9.5.2

Упражнение 9.43. Напишите функцию, получающую три строки: s, oldVal и newVal. Используя итераторы, а также функции insert(), и erase() замените все присутствующие в строке s экземпляры строки oldVal строкой newVal. Проверьте функцию на примере замены таких общепринятых сокращений, как "tho" на "though" и "thru" на "through".

Упражнение 9.44. Перепишите предыдущую функцию так, чтобы использовались индекс и функция replace().

Упражнение 9.45. Напишите функцию, получающую строку, представляющую имя и две другие строки, представляющие префикс, такой, как "Mr." или "Ms.", а также суффикс, такой, как "Jr." или "III". Используя итераторы, а также функции insert() и append(), создайте новую строку с суффиксом и префиксом, добавленным к имени.

Упражнение 9.46. Перепишите предыдущее упражнение, используя на сей раз позицию, длину и функцию insert().

 

9.5.3. Операции поиска строк

Класс string предоставляет шесть вариантов функций поиска, у каждой из которых есть четыре перегруженных версии. Функции-члены поиска и их аргументы описаны в табл. 9.14. Каждая из них возвращает значение типа string::size_type, являющееся индексом найденного элемента. Если соответствие не найдено, функции возвращают статический член (см. раздел 7.6) по имени string::npos. Библиотека определяет значение npos как -1 типа const string::size_type. Поскольку npos имеет беззнаковый тип, это означает, что значение npos соответствует наибольшему возможному размеру, который может иметь любая строка (см. раздел 2.1.2).

Таблица 9.14. Строковые функции поиска

Функции поиска возвращают индекс искомого символа или значение npos , если искомое не найдено
s.find( args ) Ищет первое местоположение аргумента args в строке s
s.rfind( args ) Ищет последнее местоположение аргумента args в строке s
s.find_first_of( args ) Ищет первое местоположение любого символа аргумента args в строке s
s.find_last_of( args ) Ищет последнее местоположение любого символа аргумента args в строке s
s.find_first_not_of( args ) Ищет первое местоположение символа в строке s , который отсутствует в аргументе args
s.find_last_not_of( args ) Ищет последнее местоположение символа в строке s , который отсутствует в аргументе args
Аргумент args может быть следующим
с , pos Поиск символа с , начиная с позиции pos в строке s . По умолчанию pos имеет значение 0
s2 , pos Поиск строки s2 , начиная с позиции pos в строке s . По умолчанию pos имеет значение 0
cp , pos Поиск строки с завершающим нулевым символом в стиле С, на которую указывает указатель cp . Поиск начинается с позиции pos в строке s . По умолчанию pos имеет значение 0
cp , pos , n Поиск первых n символов в массиве, на который указывает указатель cp . Поиск начинается с позиции pos в строке s . Аргумент pos и n не имеет значения по умолчанию

Функции поиска строк возвращают значение беззнакового типа string::size_type. Поэтому не следует использовать переменную типа int или другого знакового типа для содержания значения, возвращаемого этими функциями (см. раздел 2.1.2).

Самой простой является функция find(). Она ищет первое местоположение переданного аргумента и возвращает его индекс или значение npos, если соответствующее значение не найдено:

string name("AnnaBelle");

auto pos1 = name.find("Anna"); // pos1 == 0

Возвращает значение 0, т.е. индекс, по которому подстрока "Anna" расположена в строке "AnnaBelle".

Поиск (и другие операции со строками) чувствительны к регистру. При поиске в строке регистр имеет значение:

string lowercase("annabelle");

pos1 = lowercase.find("Anna"); // pos1 == npos

Этот код присвоит переменной pos1 значение npos, поскольку строка "Anna" не соответствует строке "anna".

Немного сложней искать соответствие любому символу в строке. Например, следующий код находит первую цифру в переменной name:

string numbers("0123456789"), name("r2d2");

// возвращает 1, т.е. индекс первой цифры в имени

auto pos = name.find_first_of(numbers);

Кроме поиска соответствия, вызвав функцию find_first_not_of(), можно искать первую позицию, которая не соответствует искомому аргументу. Например, для поиска первого нечислового символа в строке можно использовать следующий код:

string dept("03714p3");

// возвращает 5 - индекс символа 'p'

auto pos = dept.find_first_not_of(numbers);

Откуда начинать поиск

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

string::size_type pos = 0;

// каждая итерация находит следующее число в имени

while ((pos = name.find_first_of(numbers, pos))

        != string::npos) {

 cout << "found number at index: " << pos

      << " element is " << name[pos] << endl;

 ++pos; // перевести на следующий символ

}

Условие цикла while присваивает переменной pos индекс первой встретившейся цифры, начиная с текущей позиции pos. Пока функция find_first_of() возвращает допустимый индекс, результат отображается, а значение pos увеличивается.

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

Поиск в обратном направлении

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

string river("Mississippi");

auto first_pos = river.find("is"); // возвращает 1

auto last_pos = river.rfind("is"); // возвращает 4

Функция find() возвращает индекс 1, указывая, что подстрока "is" первый раз встречается, начиная с позиции 1, а функция rfind() возвращает индекс 4, указывая начало последнего местонахождения подстроки "is".

Функция find_last() аналогична функции find_first(), но возвращает последнее местоположение, а не первое.

• Функция find_last_of() ищет последний символ, который соответствует любому элементу искомой строки.

• Функция find_last_not_of() ищет последний символ, который не соответствует ни одному элементу искомой строки.

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

Упражнения раздела 9.5.3

Упражнение 9.47. Напишите программу, которая находит в строке "ab2c3d7R4E6" каждую цифру, а затем каждую букву. Напишите две версии программы: с использованием функции find_first_of() и функции find_first_not_of().

Упражнение 9.48. С учетом определения переменных name = "r2d2" и numbers = "0123456789", что возвращает вызов функции numbers.find(name)?

Упражнение 9.49. У символов может быть надстрочная часть, расположенная выше середины строки, как у d или f, или подстрочная, ниже середины строки, как у p или g. Напишите программу, которая читает содержащий слова файл и сообщает самое длинное слово, не содержащее ни надстрочных, ни подстрочных элементов.

 

9.5.4. Сравнение строк

Кроме операторов сравнения (см. раздел 3.2.2), библиотека string предоставляет набор функций сравнения, подобных функции strcmp() библиотеки С (см. раздел 3.5.4). Подобно функции strcmp(), функция s.compare() возвращает нуль, положительное или отрицательное значение, в зависимости от того, равна ли строка s, больше или меньше строки, переданной ее аргументом.

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

Таблица 9.15. Возможные аргументы функции s.compare()

s2 Сравнивает строку s со строкой s2
pos1 , n1 , s2 Сравнивает n1 символов, начиная с позиции pos1 из строки s , со строкой s2
pos1 , n1 , s2 , pos2 , n2 Сравнивает n1 символов, начиная с позиции pos1 из строки s , со строкой s2 , начиная с позиции pos2 в строке s2
cp Сравнивает строку s с завершаемым нулевым символом массивом, на который указывает указатель cp
pos1 , n1 , cp Сравнивает n1 символов, начиная с позиции pos1 из строки s , со строкой cp
pos1 , n1 , cp , n2 Сравнивает n1 символов, начиная с позиции pos1 из строки s , со строкой cp , начиная с символа n2  

 

9.5.5. Числовые преобразования

Строки зачастую содержат символы, которые представляют числа. Например, числовое значение 15 можно представить как строку с двумя символами, '1' и '5'. На самом деле символьное представление числа отличается от его числового значения. Числовое значение 15, хранимое в 16-разрядной переменной типа short, будет иметь двоичное значение 0000000000001111, а символьная строка "15", представленная как два символа из набора Latin-1, будет иметь двоичное значение 0011000100110101. Первый байт представляет символ '1', восьмеричное значение которого составит 061, а второй байт, представляющий символ '5', в наборе Latin-1 имеет восьмеричное значение 065.

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

Таблица 9.16. Преобразования между строками и числами

to_string(val) Перегруженные версии функции возвращают строковое представление значения val . Аргумент val может иметь любой арифметический тип (см. раздел 2.1.1). Есть версии функции to_string() для любого типа с плавающей точкой и целочисленного типа, включая тип int и большие типы. Малые целочисленные типы преобразуются, как обычно (см. раздел 4.11.1)
stoi(s, p, b) stol(s, p, b) stoul(s, p, b) stoll(s, p, b) stoull(s, p, b) Возвращают числовое содержимое исходной подстроки s как тип  int , long , unsigned long , long long или unsigned long long  соответственно. Аргумент b задает используемое для преобразования основание числа; по умолчанию принято значение 10. Аргумент p — указатель на тип size_t , означающий индекс первого нечислового символа в строке s ; по умолчанию p имеет значение 0. В этом случае функция не хранит индекс
stof(s, p) stod(s, p) stold(s, p) Возвращают числовое содержимое исходной подстроки s как тип float , double или long double соответственно. Аргумент p имеет то же назначение, что и у целочисленных преобразований

int i = 42;

string s = to_string(i); // преобразует переменную i типа int в ее

                         // символьное представление

double d = stod(s);      // преобразует строку s в значение типа double

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

Первый преобразуемый в числовое значение символ строки должен быть цифрой:

string s2 = "pi = 3.14";

// d = 3.14 преобразуется первая подстрока в строке s, начинающаяся

// с цифры; d = 3.14

d = stod(s2.substr(s2.find_first_of("+-.0123456789")));

Для получения позиции первого символа строки s, который мог быть частью числа, в этом вызове функции stod() используется функция find_first_of() (см. раздел 9.5.3). Функции stod() передается подстрока строки s, начиная с этой позиции. Функция stod() читает переданную строку до тех пор, пока не встретится символ, который не может быть частью числа. Затем найденное символьное представление числа преобразуется в соответствующее значение типа double.

Первый преобразуемый символ строки должен быть знаком + или - либо цифрой. Строка может начаться с части 0x или 0X, означающей шестнадцатеричный формат. У функций преобразования чисел с плавающей точкой строка может также начинаться с десятичной точки (.) и содержать символ е или E, означающий экспоненциальную часть. У функций преобразования в целочисленный тип, в зависимости от основания, строка может содержать алфавитные символы, соответствующие цифрам после цифры 9.

Если строка не может быть преобразована в число, эти функции передают исключение invalid_argument (см. раздел 5.6). Если преобразование создает значение, которое не может быть представлено заданным типом, они передают исключение out_of_range.

Упражнения раздела 9.5.5

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

Упражнение 9.51. Напишите класс, у которого есть три беззнаковых члена, представляющих год, месяц и день. Напишите конструктор, получающий строку, представляющую дату. Конструктор должен понимать множество форматов даты, такие как January 1,1900, 1/1/1900, Jan 1,1900 и т.д.

 

9.6. Адаптеры контейнеров

Кроме последовательных контейнеров, библиотека предоставляет три адаптера последовательного контейнера: stack (стек), queue (очередь) и priority_queue (приоритетная очередь). Адаптер (adaptor) — это фундаментальная концепция библиотеки. Существуют адаптеры контейнера, итератора и функции. По существу, адаптер — это механизм, заставляющий нечто одно действовать как нечто другое. Адаптер контейнера получает контейнер существующего типа и заставляет его действовать как другой. Например, адаптер stack получает любой из последовательных контейнеров (array и forward_list) и заставляет его работать подобно стеку. Функции и типы данных, общие для всех адаптеров контейнеров, перечислены в табл. 9.17.

Таблица 9.17. Функции и типы, общие для всех адаптеров

size_type Тип данных, достаточно большой, чтобы содержать размер самого большого объекта этого типа
value_type Тип элемента
container_type Тип контейнера, на базе которого реализован адаптер
A a; Создает новый пустой адаптер по имени a
A a(c); Создает новый адаптер по имени а , содержащий копию контейнера с
операторы сравнения Каждый адаптер поддерживает все операторы сравнения: == , != , < , <= , > и >= . Эти операторы возвращают результат сравнения внутренних контейнеров
a.empty() Возвращает значение true , если адаптер а пуст, и значение false в противном случае
a.size() Возвращает количество элементов в адаптере a
swap(a, b) a.swap(b) Меняет содержимое контейнеров а и b ; у них должен быть одинаковый тип, включая тип контейнера, на основании которого они реализованы

Определение адаптера

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

stack stk(deq); // копирует элементы из deq в stk

По умолчанию оба адаптера, stack и queue, реализованы на основании контейнера deque, а адаптер priority_queue реализован на базе контейнера vector. Заданный по умолчанию тип контейнера можно переопределить, указав последовательный контейнер как второй аргумент при создании адаптера:

// пустой стек, реализованный поверх вектора

stack> str_stk;

// str_stk2 реализован поверх вектора и первоначально содержит копию

svec stack> str_stk2(svec);

Существуют некоторые ограничения на применение контейнеров с определенными адаптерами. Всем адаптерам нужна возможность добавлять и удалять элементы. В результате они не могут быть основаны на массиве. Точно так же нельзя использовать контейнер forward_list, поскольку все адаптеры требуют функций добавления, удаления и обращения к последнему элементу контейнера. Стек требует только функций push_back(), pop_back() и back(), поэтому для стека можно использовать контейнер любого из остальных типов. Адаптеру queue требуются функции back(), push_back(), front() и push_front(), поэтому он может быть создан на основании контейнеров list и deque, но не vector. Адаптеру priority_queue в дополнение к функциям front(), push_back() и pop_back() требуется произвольный доступ к элементам; он может быть основан на контейнерах vector и deque, но не list.

Адаптер stack

Тип stack определен в заголовке stack. Функции-члены класса stack перечислены в табл. 9.18. Следующая программа иллюстрирует использование адаптера stack:

stack intStack; // пустой стек

// заполнить стек

for (size_t ix = 0; ix != 10; ++ix)

 intStack.push(ix); // intStack содержит значения 0...9

while (!intStack.empty()) { // пока в intStack есть значения

 int value = intStack.top();

 // код, использующий, значение

 intStack.pop(); // извлечь верхний элемент и повторить

}

Сначала intStack объявляется как пустой стек целочисленных элементов:

stack intStack; // пустой стек

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

Таблица 9.18. Функции, поддерживаемые адаптером контейнера stack , кроме приведенных в табл. 9.17

По умолчанию используется контейнер deque , но может быть также реализован на основании контейнеров list или vector .
s.pop() Удаляет, но не возвращает верхний элемент из стека
s.push( item ) s.emplace( args ) Создает в стеке новый верхний элемент, копируя или перемещая элемент item либо создавая элемент из аргумента параметра args  
s.top() Возвращает, но не удаляет верхний элемент из стека

Каждый адаптер контейнера определяет собственные функции, исходя из функций, предоставленных базовым контейнером. Использовать можно только функции адаптера, а функции основного контейнера использовать нельзя. Рассмотрим, например, вызов функции push_back() контейнера deque, на котором основан стек intStack:

intStack.push(ix); // intStack содержит значения 0...9

Хотя стек реализован на основании контейнера deque, прямого доступа к его функциям нет. Для стека нельзя вызвать функцию push_back(); вместо нее следует использовать функцию push().

Адаптеры очередей

Адаптеры queue и priority_queue определены в заголовке queue. Список функций, поддерживаемых этими типами, приведен в табл. 9.19.

Таблица 9.19. Функции адаптеров queue и priority_queue , кроме приведенных в табл. 9.17  

По умолчанию адаптер queue использует контейнер deque , а адаптер priority_queue — контейнер vector ; адаптер queue может использовать также контейнер list или vector , адаптер priority_queue может использовать контейнер deque .
q.pop() Удаляет, но не возвращает первый или наиболее приоритетный элемент из очереди или приоритетной очереди соответственно
q.front() q.back() Возвращает, но не удаляет первый или последний элемент очереди q . Допустимо только для адаптера queue
q.top() Возвращает, но не удаляет элемент с самым высоким приоритетом. Допустимо только для адаптера priority_queue
q.push( item ) q.emplace( args ) Создает элемент со значением item или создает его исходя из аргумента args в конце очереди или в соответствующей позиции приоритетной очереди

Библиотечный класс queue использует хранилище, организованное по принципу "первым пришел, первым вышел" (first-in, first-out — FIFO). Поступающие в очередь объекты помещаются в ее конец, а извлекаются из ее начала.

Адаптер priority_queue позволяет установить приоритет хранимых элементов. Добавляемые элементы помещаются перед элементами с более низким приоритетом. По умолчанию для определения относительных приоритетов в библиотеке используется оператор <. Его переопределение рассматривается в разделе 11.2.2.

Упражнения раздела 9.6

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

 

Резюме

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

Все контейнеры (кроме контейнера array) обеспечивают эффективное управление динамической памятью. Можно добавлять элементы в контейнер, не волнуясь о том, где хранить элементы. Контейнер сам управляет хранением. Контейнеры vector и string обеспечивают более подробное управление памятью, предоставляя функции-члены reserve() и capacity().

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

Функции контейнеров, которые добавляют или удаляют элементы, способны сделать существующие итераторы, указатели или ссылки некорректными. Большинство функций, которые способны сделать итераторы недопустимыми, например insert() или erase(), возвращают новый итератор, позволяющий не потерять позицию в контейнере. Особую осторожность следует соблюдать в циклах, которые используют итераторы и операции с контейнерами, способные изменить их размер.

 

Термины

Адаптерpriority_queue (приоритетная очередь). Адаптер последовательных контейнеров, позволяющий создать очередь, в которой элементы добавляются не в конец, а согласно определенному уровню приоритета. По умолчанию при определении приоритета используется оператор "меньше" для типа элемента.

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

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

Адаптер (adaptor). Библиотечный тип, функция или итератор, который заставляет один объект действовать подобно другому. Для последовательных контейнеров существуют три адаптера: stack, queue и priority_queue, каждый из которых определяет новый интерфейс базового последовательного контейнера.

Диапазон итераторов (iterator range). Диапазон элементов, обозначенный двумя итераторами. Первый итератор относится к первому элементу в последовательности, а второй — к следующему элементу после последнего. Если диапазон пуст, итераторы равны (и наоборот, если итераторы равны, они обозначают пустой диапазон). Если диапазон не пуст, второй итератор можно достичь последовательным увеличением первого итератора. Последовательное приращение итератора позволяет обработать каждый элемент диапазона.

Интервал, включающий левый элемент (left-inclusive interval). Диапазон значений, включающий первый элемент, но исключающий последний. Обычно обозначается как [i, j), т.е. начальное значение последовательности i включено, а последнее, j, исключено.

Итератор после конца (off-the-end iterator). Итератор, обозначающий (несуществующий) следующий элемент после последнего в диапазоне. Обычно называемый "конечным итератором" (end iterator).

Итератор после начала (off-the-beginning iterator). Итератор, обозначающий (несуществующий) элемент непосредственно перед началом контейнера forward_list. Возвращается функцией before_begin() контейнера forward_list. Подобно итератору, возвращенному функцией end(), к значению данного итератора обратиться нельзя.

Контейнерarray (массив). Последовательный контейнер фиксированного размера. Чтобы определить массив, кроме определения типа элемента следует указать его размер. К элементам массива можно обратиться по их позиционному индексу. Обеспечивает быстрый произвольный доступ к элементам.

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

Контейнерforward_list (односвязный список). Последовательный контейнер, представляющий односвязный список. К элементам контейнера forward_list можно обращаться только последовательно; начиная с данного элемента, можно добраться до другого элемента, только перебрав каждый элемент между ними. Итераторы класса forward_list не поддерживают декремент (--). Обеспечивает быструю вставку (или удаление) в любой позиции. В отличие от других контейнеров, вставка и удаление происходят после указанной позиции итератора. Как следствие, у контейнера forward_list есть итератор перед началом, для согласованности с обычным итератором после конца. При добавлении новых элементов итераторы остаются допустимыми. Когда элемент удаляется, некорректным становится лишь итератор удаленного элемента.

Контейнерlist (список). Последовательный контейнер, к элементам которого можно обратиться только последовательно, т.е. начиная с одного элемента, можно перейти к другому, увеличивая или уменьшая итератор. Итераторы контейнера list поддерживают как инкремент (++), так и декремент (--). Обеспечивает быструю вставку (и удаление) в любой позиции. Добавление новых элементов никак не влияет ни на другие элементы, ни на существующие итераторы. Когда элемент удаляется, некорректным становится лишь итератор удаленного элемента.

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

Контейнер (container). Тип (класс), который содержит коллекцию объектов определенного типа. Каждый библиотечный контейнер является шаблоном класса. Чтобы создать контейнер, необходимо указать тип хранимых в нем элементов. За исключением массивов, библиотечные контейнеры имеют переменный размер.

Последовательный контейнер (sequential container). Контейнер, позволяющий содержать упорядоченную коллекцию объектов одинакового типа. К элементам последовательного контейнера обращаются по позиции.

Функцияbegin(). Функция контейнера, возвращающая итератор на первый элемент в контейнере (если он есть), или итератор после конца, если контейнер пуст. Будет ли возвращенный итератор константным, зависит от типа контейнера.

Функцияcbegin(). Функция контейнера, возвращающая итератор const_iterator на первый элемент в контейнере (если он есть), или итератор после конца (off-the-end iterator), если контейнер пуст.

Функцияcend(). Функция контейнера, возвращающая итератор const_iterator на (несуществующий) элемент после последнего элемента контейнера.

Функцияend(). Функция контейнера, возвращающая итератор на (несуществующий) элемент после последнего элемента контейнера. Будет ли возвращенный итератор константным, зависит от типа контейнера.

 

Глава 10

Обобщенные алгоритмы

 

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

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

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

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

Чтобы не создавать каждую из этих функций как член контейнера каждого типа, стандартная библиотека определяет набор обобщенных алгоритмов (generic algorithm). Алгоритмами они называются потому, что реализуют классические алгоритмы, такие как сортировка и поиск, а обобщенными — потому, что работают с контейнерами любых типов, включая массивы встроенных типов, и, как будет продемонстрировано далее, с последовательностями других видов, а не только с такими библиотечными типами, как vector или list.

 

10.1. Краткий обзор

Большинство алгоритмов определено в заголовке algorithm. Библиотека определяет также набор обобщенных числовых алгоритмов в заголовке numeric.

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

int val = 42; // искомое значение

// result обозначит искомый элемент, если он есть в векторе,

// или значение vec.cend(), если нет

auto result = find(vec.cbegin(), vec.cend(), val);

// отображение результата

cout << "The value " << val

     << (result == vec.cend()

         ? " is not present" : " is present") << endl;

Первые два аргумента функции find() являются итераторами, обозначающими диапазон элементов, а третий аргумент — это значение. Функция find() сравнивает каждый элемент указанного диапазона с заданным значением и возвращает итератор на первый элемент, соответствующий этому значению. При отсутствии соответствия функция find() возвращает свой второй итератор, означая неудачу поиска. Так, сравнив возвращаемое значение со вторым аргументом, можно определить, был ли найден элемент. Для проверки, свидетельствующей об успехе поиска значения, в операторе вывода используется условный оператор (см. раздел 4.7).

Поскольку функция find() работает с итераторами, ее можно использовать для поиска значения в любом контейнере. Рассмотрим пример применения функции find() для поиска значения в списке строк:

string val = "a value"; // искомое значение

// вызов, позволяющий найти строковый элемент в списке

auto result = find(lst.cbegin(), lst.cend(), val);

Аналогично, поскольку указатели действуют как итераторы встроенных массивов, функцию find() можно использовать для поиска в массиве:

int ia[] = {27, 210, 12, 47, 109, 83};

int val = 83;

int* result = find(begin(ia), end(ia), val);

Здесь для передачи указателей на первый и следующий после последнего элементы массива ia используются библиотечные функции begin() и end() (см. раздел 3.5.3).

Искать также можно в диапазоне, заданном переданными итераторами (или указателями), на его первый и следующий после последнего элементы. Например, следующий вызов ищет соответствие в элементах iа[1]На самом деле препроцессору. Компилятор получит готовый промежуточный файл, в состав которого войдет содержимое подключенной библиотеки. — Примеч. ред .
, ia[2]А также для временного отключения больших фрагментов кода при отладке. — Примеч. ред .
и ia[3]Согласно другой трактовке, исключение — это объект системного или пользовательского класса, создаваемого операционной системой или кодом программы в ответ на обстоятельства, либо не допускающие дальнейшего нормального выполнения программы, либо определенные пользователем. Обработка исключений в приложении позволяет корректно выйти из затруднительной ситуации. — Примеч. ред .
:

// искать среди элементов, начиная с ia[1]На самом деле препроцессору. Компилятор получит готовый промежуточный файл, в состав которого войдет содержимое подключенной библиотеки. — Примеч. ред .
и до, но не включая, ia[4]Здесь и везде в оригинале именно adapt o r, а не adapt e r. — Примеч. ред .

auto result = find(ia +1, ia + 4, val);

Как работают алгоритмы

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

1. Обратиться к первому элементу последовательности.

2. Сравнить этот элемент с искомым значением.

3. Если элемент соответствует искомому, функция find() возвращает значение, идентифицирующее этот элемент.

4. В противном случае функция find() переходит к следующему элементу и повторяет этапы 2 и 3.

5. По достижении конца последовательности функция find() должна остановиться.

6. Достигнув конца последовательности, функция find() должна возвратить значение, означающее неудачу поиска. Тип этого значения должен быть совместимым с типом значения, возвращенного на этапе 3.

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

Итераторы делают алгоритмы независимыми от типа контейнера…

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

…но алгоритмы зависят от типа элементов

Хотя итераторы делают алгоритмы независимыми от контейнеров, большинство алгоритмов используют одну (или больше) функцию типа элемента. Например, этап 2 использует оператор == типа элемента для сравнения каждого элемента с предоставленным значением.

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

Упражнения раздела 10.1

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

Упражнение 10.2. Повторите предыдущую программу, но чтение значений организуйте в список (list) строк.

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

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

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

 

10.2. Первый взгляд на алгоритмы

 

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

За небольшим исключением, все алгоритмы работают с диапазоном элементов. Далее этот диапазон мы будем называть исходным диапазоном (input range). Алгоритмы, работающие с исходным диапазоном, всегда получают его в виде двух первых параметров. Эти параметры являются итераторами, используемыми для обозначения первого и следующего после последнего элемента, подлежащих обработке.

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

 

10.2.1. Алгоритмы только для чтения

Много алгоритмов только читают значения элементов в исходном диапазоне, но никогда не записывают их. Функция find() и функция count(), использованная в упражнениях раздела 10.1, являются примерами таких алгоритмов.

Другим предназначенным только для чтения алгоритмом является accumulate(), который определен в заголовке numeric. Функция accumulate() получает три аргумента. Первые два определяют диапазон суммируемых элементов, а третий — исходное значение для суммы. Предположим, что vec — это последовательность целых чисел.

// суммирует элементы вектора vec, начиная со значения 0

int sum = accumulate(vec.cbegin(), vec.cend(), 0);

Приведенный выше код суммирует значения элементов вектора vec, используя 0 как начальное значение суммы.

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

Алгоритмы и типы элементов

У того факта, что функция accumulate() использует свой третий аргумент как отправную точку для суммирования, есть важное последствие: он позволяет добавить тип элемента к типу суммы. Таким образом, тип элементов последовательности должен соответствовать или быть приводим к типу третьего аргумента. В этом примере элементами вектора vec могли бы быть целые числа, или числа типа double, или long long, или любого другого типа, который может быть добавлен к значению типа int.

Например, поскольку тип string имеет оператор +, функцию accumulate() можно использовать для конкатенации элементов вектора строк:

string sum = accumulate(v.cbegin(), v.cend(), string(""));

Этот вызов добавляет каждый элемент вектора v к первоначально пустой строке sum. Обратите внимание: третий параметр здесь явно указан как объект класса string. Передача строки как символьного литерала привела бы к ошибке при компиляции.

// ошибка: no + on const char*

string sum = accumulate(v.cbegin(), v.cend(), "");

Если бы был передан строковый литерал, типом суммируемых значений оказался бы const char*. Этот тип и определяет используемый оператор +. Поскольку тип const char* не имеет оператора +, этот вызов не будет компилироваться.

С алгоритмами, которые читают, но не пишут в элементы, обычно лучше использовать функции cbegin() и cend() (см. раздел 9.2.3). Но если возвращенный алгоритмом итератор планируется использовать для изменения значения элемента, то следует использовать функции begin() и end().

#magnify.png Алгоритмы, работающие с двумя последовательностями

Еще один алгоритм только для чтения, equal(), позволяет определять, содержат ли две последовательности одинаковые значения. Он сравнивает каждый элемент первой последовательности с соответствующим элементом второй. Алгоритм возвращает значение true, если соответствующие элементы равны, и значение false в противном случае. Он получает три итератора: первые два (как обычно) обозначают диапазон элементов первой последовательности, а третий — первый элемент второй последовательности:

// roster2 должен иметь по крайней мере столько же элементов,

// сколько и roster1

equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

Поскольку функция equal() работает с итераторами, ее можно вызвать для сравнения элементов контейнеров разных типов. Кроме того, типы элементов также не обязаны совпадать, пока можно использовать оператор == для их сравнения. Например, контейнер roster1 мог быть вектором vector, а контейнер roster2 — списком list.

Однако алгоритм equal() делает критически важное предположение: подразумевает, что вторая последовательность по крайней мере не меньше первой. Этот алгоритм просматривает каждый элемент первой последовательности и подразумевает, что для него есть соответствующий элемент во второй последовательности.

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

Упражнения раздела 10.2.1

Упражнение 10.3. Примените функцию accumulate() для суммирования элементов вектора vector.

Упражнение 10.4. Если вектор v имеет тип vector, в чем состоит ошибка вызова accumulate(v.cbegin(), v.cend(), 0) (если она есть)?

Упражнение 10.5. Что произойдет, если в вызове функции equal() для списков оба из них будут содержать строки в стиле С, а не библиотечные строки?

Ключевая концепция. Итераторы, передаваемые в качестве аргументов

Некоторые алгоритмы читают элементы из двух последовательностей. Составляющие эти последовательности элементы могут храниться в контейнерах различных видов. Например, первая последовательность могла бы быть вектором ( vector ), а вторая списком ( list ), двухсторонней очередью ( deque ), встроенным массивом или другой последовательностью. Кроме того, типы элементов этих последовательностей не обязаны совпадать точно. Обязательно необходима возможность сравнивать элементы этих двух последовательностей. Например, в алгоритме equal() типы элемента не обязаны быть идентичными, на самом деле нужна возможность использовать оператор == для сравнения элементов этих двух последовательностей.

Алгоритмы, работающие с двумя последовательностями, отличаются способом передачи второй последовательности. Некоторые алгоритмы, такие как equal() , получают три итератора: первые два обозначают диапазон первой последовательности, а третий — обозначает первый элемент во второй последовательности. Другие получают четыре итератора: первые два обозначают диапазон элементов в первой последовательности, а вторые два — диапазон второй последовательности.

Алгоритмы, использующие для обозначения второй последовательности один итератор, подразумевают , что вторая последовательность по крайней мере не меньше первой. Разработчик должен сам позаботиться о том, чтобы алгоритм не пытался обратиться к несуществующим элементам во второй последовательности. Например, алгоритм equal() сравнивает каждый элемент первой последовательности с соответствующим элементом второй. Если вторая последовательность является подмножеством первой, то возникает серьезная ошибка — функция equal() попытается обратиться к элементам после конца второй последовательности.

 

10.2.2. Алгоритмы, записывающие элементы контейнера

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

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

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

fill(vec.begin(), vec.end(), 0); // обнулить каждый элемент

// присвоить половине последовательности значение 10

fill(vec.begin(), vec.begin() + vec.size()/2, 10);

Поскольку функция fill() пишет в переданную ей исходную последовательность до тех пор, пока она не закончится, запись вполне безопасна.

#magnify.png Алгоритмы не проверяют операции записи

Некоторые алгоритмы получают итератор, обозначающий конкретное назначение. Эти алгоритмы присваивают новые значения элементам последовательности, начиная с элемента, обозначенного итератором назначения. Например, функция fill_n() получает один итератор, количество и значение. Она присваивает предоставленное значение заданному количеству элементов, начиная с элемента, обозначенного итератором. Функцию fill_n() можно использовать для присвоения нового значения элементам вектора:

vector vec; // пустой вектор

// используя вектор vec, предоставить ему разные значения

fill_n(vec.begin(), vec.size(), 0); // обнулить каждый элемент vec

Функция fill_n() подразумевала, что безопасно запишет указанное количество элементов. Таким образом, следующий вызов функции fill_n() подразумевает, что dest указывает на существующий элемент и что в последовательности есть по крайней мере n элементов, начиная с элемента dest.

fill_n(dest, n, val)

Это вполне обычная ошибка для новичка: вызов функции fill_n() (или подобного алгоритма записи элементов) для контейнера без элементов:

vector vec; // пустой вектор

// катастрофа: попытка записи в 10 несуществующих элементов

// вектора vec

fill_n(vec.begin(), 10, 0);

Этот вызов функции fill_n() ведет к катастрофе. Должно быть записано десять элементов, но вектор vec пуст, и никаких элементов в нем нет. Результат непредсказуем, но, вероятнее всего, произойдет серьезный отказ во время выполнения.

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

Функция back_inserter()

Один из способов проверки, имеет ли контейнер достаточно элементов для записи, подразумевает использование итератора вставки (insert iterator), который позволяет добавлять элементы в базовый контейнер. Как правило, при присвоении значения элементу контейнера при помощи итератора осуществляется присвоение тому элементу, на который указывает итератор. При присвоении с использованием итератора вставки в контейнер добавляется новый элемент, равный правому значению.

Более подробная информация об итераторе вставки приведена в разделе 10.4.1. Однако для иллюстрации безопасного применения алгоритмов, записывающих данные в контейнер, используем функцию back_inserter(), определенную в заголовке iterator.

Функция back_inserter() получает ссылку на контейнер и возвращает итератор вставки, связанный с данным контейнером. Попытка присвоения значения элементу при помощи этого итератора приводит к вызову функции push_back(), добавляющей элемент с данным значением в контейнер.

vector vec; // пустой вектор

auto it = back_inserter(vec); // присвоение при помощи it добавляет

                              // элементы в vec

*it = 42; // теперь vec содержит один элемент со значением 42

Функцию back_inserter() зачастую применяют для создания итератора, используемого в качестве итератора назначения алгоритмов. Рассмотрим пример:

vector vec; // пустой вектор

// ok: функция back_inserter() создает итератор вставки,

// который добавляет элементы в вектор vec

fill_n(back_inserter(vec), 10, 0); // добавляет 10 элементов в vec

На каждой итерации функция fill_n() присваивает элемент заданной последовательности. Поскольку ей передается итератор, возвращенный функцией back_inserter(), каждое присвоение вызовет функцию push_back() вектора vec. В результате этот вызов функции fill_n() добавит в конец вектора vec десять элементов, каждый со значением 0.

Алгоритм copy()

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

В качестве примера используем функцию copy() для копирования одного встроенного массива в другой:

int a1[] = {0,1,2,3,4,5,6,7,8,9};

int а2[sizeof(a1)/sizeof(*a1)]; // a2 имеет тот же размер, что и a1

// указывает на следующий элемент после последнего скопированного в а2

auto ret = copy(begin(a1), end(a1), a2); // копирует a1 в a2

Здесь определяется массив по имени a2, а функция sizeof() используется для гарантии равенства размеров массивов а2 и a1 (см. раздел 4.9). Затем происходит вызов функции copy() для копирования массива a1 в массив а2. После вызова у элементов обоих массивов будут одинаковые значения.

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

Некоторые алгоритмы обладают так называемой версией "копирования". Эти алгоритмы осуществляют некую обработку элементов исходной последовательности, но саму последовательность не изменяют. Они могут создавать новую последовательность, в которую и сохраняют результат обработки элементов исходной.

Например, алгоритм replace() читает последовательность и заменяет каждый экземпляр заданного значения другим значением. Алгоритм получает четыре параметра: два итератора, обозначающих исходный диапазон, и два значения. Он заменяет вторым значением значение каждого элемента, которое равно первому.

// заменить во всех элементах значение 0 на 42

replace(ilst.begin(), ilst.end(), 0, 42);

Этот вызов заменяет все экземпляры со значением 0 на 42. Если исходную последовательность следует оставить неизменной, необходимо применить алгоритм replace_copy(). Этой версии функции передают третий аргумент: итератор, указывающий получателя откорректированной последовательности.

// использовать функцию back_inserter() для увеличения контейнера

// назначения до необходимых размеров

replace_copy(ilst.cbegin(), ilst.cend(),

             back_inserter(ivec), 0, 42);

После этого вызова список ilst останется неизменным, а вектор ivec будет содержать копию его элементов, но со значением 42 вместо 0.

 

10.2.3. Алгоритмы, переупорядочивающие элементы контейнера

Некоторые алгоритмы изменяют порядок элементов в пределах контейнера. Яркий пример такого алгоритма — sort(). Вызов функции sort() упорядочивает элементы исходного диапазона в порядке сортировки, используя оператор < типа элемента.

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

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

the quick red fox jumps over the slow red turtle

В результате обработки этого текста программа должна создать следующий вектор:

Устранение дубликатов

Для устранения повторяющихся слов сначала отсортируем вектор так, чтобы дубликаты располагались рядом друг с другом. После сортировки вектора можно использовать другой библиотечный алгоритм, unique(), чтобы расположить уникальные элементы в передней части вектора. Поскольку алгоритмы не могут работать с самими контейнерами, используем функцию-член erase() класса vector для фактического удаления элементов:

void elimDups(vector &words) {

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

 sort(words.begin(), words.end());

 // функция unique() переупорядочивает исходный диапазон так, чтобы

 // каждое слово присутствовало только один раз в начальной части

 // диапазона, и возвращает итератор на элемент, следующий после

 // диапазона уникальных значений

 auto end_unique = unique(words.begin(), words.end());

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

 // функцию erase() вектора

 words.erase(end_unique, words.end());

}

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

#img_341.png

Обратите внимание: слова red и the встречаются дважды.

#magnify.png Алгоритм unique()

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

#img_342.png

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

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

#magnify.png Применение функций контейнера для удаления элементов

Для фактического удаления неиспользуемых элементов следует использовать контейнерную функцию erase() (см. раздел 9.3.3). Удалению подлежит диапазон элементов от того, на который указывает итератор end_unique, и до конца контейнера words. После вызова контейнер words содержит восемь уникальных слов из исходного текста.

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

Упражнения раздела 10.2.3

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

Упражнение 10.7. Определите, есть ли ошибки в следующих фрагментах кода, и, если есть, как их исправить:

(a) vector vec; list lst; int i;

    while (cin >> i)

     lst.push_back(i);

    copy(lst.cbegin(), lst.cend(), vec.begin());

(b) vector vec;

    vec.reserve(10); // reserve рассматривается в разделе 9.4

    fill_n(vec.begin(), 10, 0);

Упражнение 10.8. Как упоминалось, алгоритмы не изменяют размер контейнеров, с которыми они работают. Почему использование функции back_inserter() не противоречит этому утверждению?

Упражнение 10.9. Реализуйте собственную версию функции elimDups(). Проверьте ее в программе, выводящей содержимое вектора после чтения ввода, после вызова функции unique() и после вызова функции erase().

Упражнение 10.10. Почему алгоритмы не изменяют размер контейнеров?

 

10.3. Перенастройка функций

 

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

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

 

10.3.1. Передача функций алгоритму

Предположим, например, что необходимо вывести вектор после вызова функции elimDups() (см. раздел 10.2.3). Однако слова должны быть упорядочены сначала по размеру, а затем в алфавитном порядке в пределах каждого размера. Чтобы переупорядочить вектор по длине слов, используем вторую перегруженную версию функции sort(). Она получает третий аргумент, называемый предикатом.

Предикаты

Предикат (predicate) — это допускающее вызов выражение, возвращающее значение, применимое в условии. Библиотечные алгоритмы используют унарные предикаты (unary predicate) (с одним параметром) или бинарные предикаты (binary predicate) (с двумя параметрами). Получающие предикаты алгоритмы вызывают его для каждого элемента в исходном диапазоне. Поэтому тип элемента должен допускать преобразование в тип параметра предиката.

Версия функции sort(), получающей бинарный предикат, использует его вместо оператора < при сравнении элементов. Предикаты, предоставляемые функции sort(), должны соответствовать требованиям, описанным в разделе 11.2.2, а пока достаточно знать, что он должен определить единообразный порядок для всех возможных элементов в исходной последовательности. Функция isShorter() из раздела 6.2.2 — хороший пример функции, соответствующей этим требованиям. Таким образом, функцию isShorter() можно передать как предикат алгоритму sort(). Это переупорядочит элементы по размеру:

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

bool isShorter(const string &s1, const string &s2) {

 return s1.size() < s2.size();

}

// сортировка слов по длине от коротких к длинным

sort(words.begin(), words.end(), isShorter);

Если контейнер words содержит те же данные, что и в разделе 10.2.3, то этот вызов переупорядочит его так, что все слова длиной 3 символа расположатся перед словами длиной 4 символа, которые в свою очередь расположатся перед словами длиной 5 символов, и т.д.

Алгоритмы сортировки

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

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

elimDups(words); // расположить слова в алфавитном порядке

// и удалить дубликаты

// пересортировать по длине, поддерживая алфавитный порядок среди слов

// той же длины

stable_sort(words.begin(), words.end(), isShorter);

for (const auto &s : words) // копировать строки не нужно

 cout << s << " "; // вывести каждый элемент, отделяя его пробелом

cout << endl;

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

fox red the over slow jumps quick turtle

Упражнения раздела 10.3.1

Упражнение 10.11. Напишите программу, использующую функции stable_sort() и isShorter() для сортировки вектора, переданного вашей версии функции elimDups(). Для проверки правильности программы выведите содержимое вектора.

Упражнение 10.12. Напишите функцию compareIsbn(), которая сравнивает члены isbn двух объектов класса Sales_data. Используйте эту функцию для сортировки вектора объектов класса Sales_data.

Упражнение 10.13. Библиотека определяет алгоритм partition(), получающий предикат и делящий контейнер так, чтобы значения, для которых предикат возвращает значение true, располагались в начале последовательности, а для которых он возвращает значение false — в конце. Алгоритм возвращает итератор на следующий элемент после последнего, для которого предикат возвратил значение true. Напишите функцию, которая получает строку и возвращает логическое значение, указывающее, содержит ли строка пять символов или больше. Используйте эту функцию для разделения вектора words. Выведите элементы, у которых есть пять или более символов.

 

10.3.2. Лямбда-выражения

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

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

Вот "эскиз" этой функции, которую мы назовем biggies():

void biggies(vector &words,

vector::size_type sz) {

 elimDups(words); // расположить слова в алфавитном порядке

                  // и удалить дубликаты

 // пересортировать по длине, поддерживая алфавитный порядок среди слов

 // той же длины

 stable_sort(words.begin(), words.end(), isShorter);

 // получить итератор на первый элемент, размер которого >= sz

 // вычислить количество элементов с размером >= sz

 // вывести слова, размер которых равен или больше заданного, разделяя

 // их пробелами

}

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

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

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

Знакомство с лямбда-выражением

Алгоритму можно передать любой вид вызываемого объекта (callable object). Объект или выражение является вызываемым, если к нему можно применить оператор вызова (см. раздел 1.5.2). Таким образом, если е — вызываемое выражение, то можно написать е( аргументы ) , где аргументы — это разделяемый запятыми список любого количества аргументов.

Единственными вызываемыми объектами, использованными до сих пор, были функции и указатели на функции (см. раздел 6.7). Есть еще два вида вызываемых объектов: классы, перегружающие оператор вызова функции (будут рассматриваться в разделе 14.8), и лямбда-выражения (lambda expression).

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

[ список захвата ]( список параметров ) -> тип возвращаемого значения

 { тело функции }

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

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

auto f = [] { return 42; };

Здесь f определено как вызываемый объект, не получающий никаких аргументов и возвращающий значение 42. Вызов лямбда-выражений происходит таким же способом, что и вызов функций, — при помощи оператора вызова:

cout << f() << endl; // выводит 42

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

Лямбда-выражения, тела которых содержат нечто кроме одного оператора return, что не определяет тип возвращаемого значения, возвращают тип void.

Передача аргументов лямбда-выражению

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

Для примера передачи аргументов можно написать лямбда-выражение, ведущее себя как функция isShorter():

[](const string &a, const string &b)

 { return a.size() < b.size();}

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

Вызов функции stable_sort() можно переписать так, чтобы использовать это лямбда-выражение следующим образом:

// сортировать слова по размеру, поддерживая алфавитный порядок среди

// слов того же размера

stable_sort(words.begin(), words.end(),

            [](const string &a, const string &b)

             { return a.size() < b.size();});

Когда функция stable_sort() будет сравнивать два элемента, она вызовет данное лямбда-выражение.

Использование списка захвата

Теперь все готово для решения первоначальной задачи — создания вызываемого выражения, которое можно передать функции find_if(). Необходимо выражение, сравнивающее длину каждой строки в исходной последовательности со значением параметра sz функции biggies().

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

В данном случае лямбда-выражение захватит переменную sz и будет иметь один параметр типа string. Тело лямбда-выражения сравнивает размер переданной строки с захваченным значением переменной sz:

[sz](const string &a)

 { return a.size () >= sz; };

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

Поскольку данное лямбда-выражение захватывает переменную sz, ее можно будет использовать в теле лямбда-выражения. Лямбда-выражение не захватывает вектор words, поэтому доступа к его переменным она не имеет. Если бы лямбда-выражение имело пустой список захвата, наш код не компилировался бы:

// ошибка: sz не захвачена

[](const string &a)

 { return a.size() >= sz; };

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

Вызов функции find_if()

Используя это лямбда-выражение, можно найти первый элемент, размер которого не меньше sz:

// получить итератор на первый элемент, размер которого >= sz

auto wc = find_if(words.begin(), words.end(),

                  [sz](const string &a)

                   { return a.size() >= sz; });

Вызов функции find_if() возвращает итератор на первый элемент, длина которого не меньше sz, или на элемент words.end(), если такового элемента не существует.

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

// вычислить количество элементов с размером >= sz

auto count = words.end() - wc;

cout << count << " " << make_plural(count, "word", "s")

     << " of length " << sz << " or longer" << endl;

Для вывода в сообщении слова word или words, в зависимости от того, равен ли размер 1, оператор вывода вызывает функцию make_plural() (см. раздел 6.3.2).

Алгоритм for_each()

Последняя часть задачи — вывод тех элементов вектора words, длина которых не меньше sz. Для этого используем алгоритм for_each(), получающий вызываемый объект и вызывающий его для каждого элемента в исходном диапазоне:

// вывести слова, размер которых равен или больше заданного, разделяя

// их пробелами

for_each(wc, words.end(),

         [](const string &s) {cout << s << " ";});

cout << endl;

Список захвата этого лямбда-выражения пуст, но все же его тело использует два имени: его собственный параметр s и cout.

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

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

Объединив все вместе

Теперь, изучив элементы программы подробно, рассмотрим ее в целом:

void biggies(vector &words,

             vector::size_type sz) {

 elimDups(words); // расположить слова в алфавитном порядке

                  // и удалить дубликаты

 // пересортировать по длине, поддерживая алфавитный порядок среди слов

 // той же длины

 stable_sort(words.begin(), words.end(),

             [](const string &a, const string &b)

              { return a.size() < b.size(); });

 // получить итератор на первый элемент, размер которого >= sz

 auto wc = find_if(words.begin(), words.end(),

                   [sz](const string &a)

                    { return a.size() >= sz; });

 // вычислить количество элементов с размером >= sz

 auto count = words.end() - wc;

 cout << count << " " << make_plural(count, "word", "s")

      << " of length " << sz << " or longer" << endl;

 // вывести слова, размер которых равен или больше заданного, разделяя

 // их пробелами

 for_each(wc, words.end(),

          [](const string &s) {cout << s << " ";});

 cout << endl;

}

Упражнения раздела 10.3.2

Упражнение 10.14. Напишите лямбда-выражение, получающее два целых числа и возвращающее их сумму.

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

Упражнение 10.16. Напишите собственную версию функции biggies(), используя лямбда-выражения.

Упражнение 10.17. Перепишите упражнение 10.12 из раздела 10.3.1 так, чтобы в вызове функции sort() вместо функции compareIsbn() использовалось лямбда-выражение.

Упражнение 10.18. Перепишите функцию biggies() так, чтобы использовать алгоритм partition() вместо алгоритма find_if(). Алгоритм partition() описан в упражнении 10.13 раздела 10.3.1.

Упражнение 10.19. Перепишите предыдущее упражнение так, чтобы использовать алгоритм stable_partition(), который, подобно алгоритму stable_sort(), обеспечивает исходный порядок элементов в разделяемой последовательности.

 

10.3.3. Захват и возвращение значений лямбда-выражениями

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

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

Захват по значению

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

void fcnl() {

 size_t v1 = 42; // локальная переменная

 // копирует v1 в вызываемый объект f

 auto f = [v1] { return v1; };

 v1 = 0;

 auto j = f(); // j = 42; f получит копию v1 на момент создания

}

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

Таблица 10.1. Список захвата лямбда-выражения

[] Пустой список захвата. Лямбда-выражение не может использовать переменные из содержащей функции. Лямбда-выражение может использовать локальные переменные, только если оно захватывает их
[ names ] names — разделяемый запятыми список имен, локальных для содержащей функции. По умолчанию переменные в списке захвата копируются. Имя, которому предшествует знак & , захватывается по ссылке
[&] Неявный захват по ссылке. Сущности из содержащей функции используются в теле лямбда-выражения по ссылке
[=] Неявный захват по значению. Сущности из содержащей функции используются в теле лямбда-выражения как копии
[&, identifier_list ] identifier_list — разделяемый запятыми список любого количества переменных из содержащей функции. Эти переменные захватываются по значению; любые неявно захваченные переменные захватывается по ссылке. Именам в списке identifier_list не могут предшествовать символы &
[=, reference_list ] Переменные, включенные в список reference_list , захватываются по ссылке; любые неявно захваченные переменные захватывается по значению. Имена в списке reference_list не могут включать часть this и должны предваряться символом &

Захват по ссылке

Можно также определять лямбда-выражения, захватывающие переменные по ссылке. Например:

void fcn2() {

 size_t v1 = 42; // локальная переменная

 // объект f2 содержит ссылку на v1

 auto f2 = [&v1] { return v1; };

 v1 = 0;

 auto j = f2(); // j = 0; f2 ссылается на v1; он не хранится в j

}

Символ & перед v1 указывает, что переменная v1 должна быть захвачена как ссылка. Захваченная по ссылке переменная действует так же, как любая другая ссылка. При использовании переменной в теле лямбда-выражения фактически применяется объект, с которым связана эта ссылка. В данном случае, когда лямбда-выражение возвращает v1, возвращается значение объекта, на который ссылается переменная v1.

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

Иногда захват по ссылке необходим. Например, может понадобиться, чтобы функция biggies() получала ссылку на поток ostream для записи символа, используемого как разделитель:

void biggies(vector &words,

             vector::size_type sz,

             ostream &os = cout, char c = ' ') {

 // код, переупорядочивающий слова как прежде

 // оператор вывода количества, пересмотренный для вывода os

 for_each(words.begin(), words.end(),

          [&os, c](const string &s) { os << s << c; });

}

Объекты потока ostream нельзя копировать (см. раздел 8.1.1); единственный способ захвата объекта os — это ссылка (или указатель).

При передаче лямбда-выражения функции, как и в случае вызова функции for_each(), лямбда-выражение выполняется немедленно. Захват объекта os по ссылке хорош потому, что переменные в функции biggies() существуют во время выполнения функции for_each().

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

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

Совет. Не усложняйте списки захвата лямбда-выражений

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

Захват обычной переменной (типа int , string и так далее, но не указателя) обычно достаточно прост. В данном случае следует позаботиться о наличии у переменной значения в момент ее захвата.

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

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

Неявный захват

Вместо предоставления явного списка переменных содержащей функции, которые предстоит использовать, можно позволить компилятору самостоятельно вывести используемые переменные из кода тела лямбда-выражения. Чтобы заставить компилятор самостоятельно вывести список захвата, в нем используется символ & или =. Символ & указывает, что предполагается захват по ссылке, а символ = — что значения захватываются по значению. Например, передаваемое функции find_if() лямбда-выражение можно переписать так:

// sz неявно захватывается по значению

wc = find_if(words.begin(), words.end(),

             [=](const string &s)

              { return s.size () >= sz; });

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

void biggies(vector &words,

             vector::size_type sz,

             ostream &os = cout, char c = ' ') {

 // другие действия, как прежде

 // os неявно захватывается по ссылке;

 // с явно захватывается по значению

 for_each(words.begin(), words.end(),

          [&, c](const string &s) { os << s << c; });

 // os явно захватывается по ссылке;

 // с неявно захватывается по значению

 for_each(words.begin(), words.end(),

          [=, &os](const string &s) { os << s << c; });

}

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

При совмещении неявного и явного захвата явно захваченные переменные должны использовать дополнительную форму. Таким образом, при неявном захвате по ссылке (с использованием &) явно именованные переменные должны захватываться по значению; следовательно, их именам не может предшествовать символ &. И наоборот, при неявном захвате по значению (с использованием =) явно именованным переменным должен предшествовать символ &, означающий, что они должны быть захвачены по ссылке.

Изменяемые лямбда-выражения

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

void fcn3() {

 size_t v1 = 42; // локальная переменная

 // f может изменить значение захваченных переменных

 auto f = [v1]() mutable { return ++v1; };

 v1 = 0;

 auto j = f(); // j = 43

}

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

void fcn4() {

 size_t v1 = 42; // локальная переменная

 // v1 - ссылка на неконстантную переменную

 // эту переменную можно изменить в f2 при помощи ссылки

 auto f2 = [&v1] { return ++v1; };

 v1 = 0;

 auto j = f2(); // j = 1

}

Определение типа возвращаемого значения лямбда-выражения

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

В качестве примера используем библиотечный алгоритм transform() и лямбда-выражение для замены каждого отрицательного значения в последовательности его абсолютным значением:

transform(vi.begin(), vi.end(), vi.begin(),

          [](int i) { return i < 0 ? -i : i; });

Функция transform() получает три итератора и вызываемый объект. Первые два итератора обозначают исходную последовательность, третий итератор — назначение. Алгоритм вызывает переданный ему вызываемый объект для каждого элемента исходной последовательности и записывает результат по назначению. Как и в данном примере, итератор назначения может быть тем же, обозначающим начало ввода. Когда исходный итератор и итератор назначения совпадают, алгоритм transform() заменяет каждый элемент в исходном диапазоне результатом вызова вызываемого объекта для этого элемента.

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

Но если написать на первый взгляд эквивалентную программу, используя оператор if, то код не будет компилироваться:

// ошибка: нельзя вывести тип возвращаемого значения лямбда-выражения

transform(vi.begin(), vi.end(), vi.begin(),

          [](int i) {if (i < 0) return -i; else return i; });

Эта версия лямбда-выражения выводит тип возвращаемого значения как void, но возвращает значение.

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

transform(vi.begin(), vi.end(), vi.begin(),

          [](int i) -> int

           { if (i < 0) return -i; else return i; });

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

Упражнения раздела 10.3.3

Упражнение 10.20. Библиотека определяет алгоритм count_if(). Подобно алгоритму find_if(), он получает пару итераторов, обозначающих исходный диапазон и предикат, применяемый к каждому элементу заданного диапазона. Функция count_if() возвращает количество раз, когда предикат вернул значение true. Используйте алгоритм count_if(), чтобы переписать ту часть программы, которая рассчитывала количество слов длиной больше 6.

Упражнение 10.21. Напишите лямбда-выражение, которое захватывает локальную переменную типа int и осуществляет декремент ее значения, пока оно не достигает 0. Как только значение переменной достигнет 0, декремент переменной прекращается. Лямбда-выражение должно возвратить логическое значение, указывающее, имеет ли захваченная переменная значение 0.

 

10.3.4. Привязка аргументов

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

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

Однако не так просто написать функцию для замены лямбда-выражений, которые захватывают локальные переменные. Рассмотрим, например, использованное в вызове функции find_if() лямбда-выражение, которое сравнивало размер строки с заданным размером. Совсем не сложно написать функцию, выполняющую те же действия:

bool check_size(const string &s, string::size_type sz) {

 return s.size() >= sz;

}

Но мы не можем использовать эту функцию как аргумент функции find_if(). Как уже упоминалось, функция find_if() получает унарный предикат, поэтому переданное ей вызываемое выражение должно получать один аргумент. Лямбда-выражение, переданное функцией biggies() функции find_if(), использует свой список захвата для хранения значения переменной sz. Чтобы использовать функцию check_size() вместо этого лямбда- выражения, следует выяснить, как передать аргумент sz параметру.

Библиотечная функция bind()

Проблему передачи аргумента размера функции check_size() можно решить при помощи новой библиотечной функции bind(), определенной в заголовке functional. Функцию bind() можно считать универсальным адаптером функции (см. раздел 9.6). Она получает вызываемый объект и создает новый вызываемый объект, который адаптирует список параметров исходного объекта.

Общая форма вызова функции bind() такова:

auto новыйВызываемыйОбъект = bind( вызываемыйОбъект , список_аргументов );

где новыйВызываемыйОбъект — это новый вызываемый объект, а список_аргументов — разделяемый запятыми список аргументов, соответствующих параметрам переданного вызываемого объекта вызываемыйОбъект . Таким образом, когда происходит вызов объекта новыйВызываемыйОбъект , он вызывает вызываемыйОбъект , передавая аргументы из списка список_аргументов .

Аргументы из списка список_аргументов могут включать имена в формате _ n , где n — целое число. Эти аргументы — знакоместа, представляющие параметры объекта новыйВызываемыйОбъект . Они располагаются вместо аргументов, которые будут переданы объекту новыйВызываемыйОбъект . Число n является позицией параметра вновь созданного вызываемого объекта: _1 — первый параметр, _2 — второй и т.д.

Привязка параметра sz к функции check_size()

В качестве примера использования функции bind() создадим объект, который вызывает функцию check_size() с фиксированным значением ее параметра размера:

// check6 - вызываемый объект, получающий один аргумент типа string

// и вызывающий функцию check_size() с этой строкой и значением 6

auto check6 = bind(check_size, _1, 6);

У этого вызова функции bind() есть только одно знакоместо, означающее, что вызываемый объект check6() получает один аргумент. Знакоместо располагается первым в списке аргументов. Это означает, что параметр вызываемого объекта check6() соответствует первому параметру функции check_size(). Этот параметр имеет тип const string&, а значит, параметр вызываемого объекта check6() также имеет тип const string&. Таким образом, при вызове check6() следует передать аргумент типа string, который вызываемый объект check6() передаст в качестве первого аргумента функции check_size().

Второй аргумент в списке аргументов (т.е. третий аргумент функции bind()) является значением 6. Это значение связывается со вторым параметром функции check_size(). Каждый раз, когда происходит вызов вызываемого объекта check6(), он передает функции check_size() значение 6 как второй аргумент:

string s = "hello";

bool b1 = check6(s); // check6(s) вызывает check_size(s, 6)

Используя функцию bind(), можно заменить следующий исходный вызов функции find_if() на базе лямбда-выражения:

auto wc = find_if(words.begin(), words.end(),

                  [sz](const string &a)

кодом, использующим функцию check_size(),

auto wc = find_if(words.begin(), words.end(),

                  bind(check_size, _1, sz));

Этот вызов функции bind() создает вызываемый объект, который привязывает второй аргумент функции check_size() к значению параметра sz. Когда функция find_if() вызовет этот объект для строк вектора words, он, в свою очередь, вызовет функцию check_size(), передав полученную строку и значение параметра sz. Таким образом, функция find_if() фактически вызовет функцию check_size() для каждой строки в исходном диапазоне и сравнит размер этой строки со значением параметра sz.

Использование имен из пространства имен placeholders

Имена формата _ n определяются в пространстве имен placeholders. Само это пространство имен определяется в пространстве имен std (см. раздел 3.1). Чтобы использовать эти имена, следует предоставить имена обоих пространств имен. Подобно нашим другим примерам, данные вызовы функции bind() подразумевали наличие соответствующих объявлений using. Рассмотрим объявление using для имени _1:

using std::placeholders::_1;

Это объявление свидетельствует о том, что используется имя _1, определенное в пространстве имен placeholders, которое само определено в пространстве имен std.

Для каждого используемого имени знакоместа следует предоставить отдельное объявление using. Но поскольку написание таких объявлений может быть утомительно и ведет к ошибкам, вместо этого можно использовать другую форму using, которая подробно рассматривается в разделе 18.2.2:

using namespace пространствоимен_имя ;

Она свидетельствует, что необходимо сделать доступными для нашей программы все имена из пространства имен пространствоимен_имя :

using namespace std::placeholders;

Этот код позволяет использовать все имена, определенные в пространстве имен placeholders. Подобно функции bind(), пространство имен placeholders определено в заголовке functional.

Аргументы функции bind()

Как уже упоминалось, функцию bind() можно использовать для фиксированного значения параметра. В более общем случае функцию bind() можно использовать для привязки или перестройки параметров в предоставленном вызываемом объекте. Предположим, например, что f() — вызываемый объект с пятью параметрами:

// g - вызываемый объект, получающий два аргумента

auto g = bind(f, a, b, _2, с, _1);

Этот вызов функции bind() создает новый вызываемый объект, получающий два аргумента, представленные знакоместами _2 и _1. Новый вызываемый объект передает собственные аргументы как третий и пятый аргументы вызываемому объекту f(). Первый, второй и четвертый аргументы вызываемого объекта f() связаны с переданными значениями a, b и с соответственно.

Аргументы вызываемого объекта g() связаны со знакоместами по позиции. Таким образом, первый аргумент вызываемого объекта g() связан с параметром _1, а второй — с параметром _2. Следовательно, когда происходит вызов g(), его первый аргумент будет передан как последний аргумент вызываемого объекта f(); второй аргумент g() будет передан как третий. В действительности этот вызов функции bind() преобразует вызов g(_1, _2) в вызов f(а, b, _2, с, _1).

Таким образом, вызов вызываемого объекта g() вызывает вызываемый объект f() с использованием аргументов вызываемого объекта g() для знакомест наряду с аргументами a, b и с. Например, вызов g(X, Y) приводит к вызову f(a, b, Y, с, X).

Использование функции bind() для переупорядочивания параметров

Рассмотрим более конкретный пример применения функции bind() для переупорядочивания аргументов. Используем ее для обращения смысла функции isShorter() следующим образом:

// сортировка по длине слов от коротких к длинным

sort(words.begin(), words.end(), isShorter);

// сортировка по длине слов от длинных к коротким

sort(words.begin(), words.end(), bind(isShorter, _2, _1));

В первом вызове, когда алгоритм sort() должен сравнить два элемента, А и В, он вызовет функцию isShorter(A, В). Во втором вызове аргументы функции isShorter() меняются местами. В данном случае, когда алгоритм sort() сравнивает элементы, он вызывает функцию isShorter(В, А).

Привязка ссылочных параметров

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

Для примера заменим лямбда-выражение, которое захватывало поток ostream по ссылке:

// os - локальная переменная, ссылающаяся на поток вывода

// с - локальная переменная типа char

for_each(words.begin(), words.end(),

         [&os, c] (const string &s) { os << s << c; });

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

ostream &print(ostream &os, const string &s, char c) {

 return os << s << c;

}

Но для замены захвата переменной os нельзя использовать функцию bind() непосредственно:

// ошибка: нельзя копировать os

for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

Поскольку функция bind() копирует свои аргументы, она не сможет скопировать поток ostream. Если объект необходимо передать функции bind(), не копируя, то следует использовать библиотечную функцию ref():

for_each(words.begin(), words.end(),

         bind(print, ref(os), _1, ' '));

Функция ref() возвращает объект, который содержит переданную ссылку, являясь при этом вполне копируемым. Существует также функция cref(), создающая класс, содержащий ссылку на константу. Подобно функции bind(), функции ref() и cref() определены в заголовке functional.

Совместимость с прежней версией: привязка аргументов

Прежние версии языка С++ имели много больше ограничений, и все же более сложный набор средств привязки аргументов к функциям. Библиотека определяет две функции — bind1st() и bind2nd() . Подобно функции bind() , эти функции получают функцию и создают новый вызываемый объект для вызова переданной функции с одним из ее параметров, связанным с переданным значением. Но эти функции могут связать только первый или второй параметр соответственно. Поскольку они имеют очень много ограничений, в новом стандарте эти функции не рекомендованы . Это устаревшее средство, которое может не поддерживаться в будущих выпусках. Современные программы С++ должны использовать функцию bind() .

Упражнения раздела 10.3.4

Упражнение 10.22. Перепишите программу подсчета слов размером 6 символов с использованием функций вместо лямбда-выражений.

Упражнение 10.23. Сколько аргументов получает функции bind()?

Упражнение 10.24. Используйте функции bind() и check_size() для поиска первого элемента вектора целых чисел, значение которого больше длины заданного строкового значения.

Упражнение 10.25. В упражнениях раздела 10.3.2 была написана версия функции biggies(), использующая алгоритм partition(). Перепишите эту функцию так, чтобы использовать функции check_size() и bind().

 

10.4. Возвращаясь к итераторам

 

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

• Итератор вставки (insert iterator). Связан с контейнером и применяется для вставки элементов в контейнер.

• Потоковый итератор (stream iterator). Может быть связан с потоком ввода или вывода и применяется для перебора связанного потока ввода- вывода.

• Реверсивный итератор (reverse iterator). Перемещается назад, а не вперед. У всех библиотечных контейнеров, кроме forward_list, есть реверсивные итераторы.

• Итератор перемещения (move iterator). Итератор специального назначения; он перемещает элементы, а не копирует. Эти итераторы рассматриваются в разделе 13.6.2.

 

10.4.1. Итераторы вставки

Адаптер вставки (inserter), или адаптер inserter, — это адаптер итератора (см. раздел 9.6), получающий контейнер и возвращающий итератор, позволяющий вставлять элементы в указанный контейнер. Присвоение значения при помощи итератора вставки приводит к вызову контейнерной функции, добавляющей элемент в определенную позицию заданного контейнера. Операторы, поддерживающие эти итераторы, приведены в табл. 10.2.

Таблица 10.2. Операторы итератора вставки

it = t Вставляет значение t в позицию, обозначенную итератором it . В зависимости от вида итератора вставки и с учетом того, что он связан с контейнером с , вызывает функции c.push_back(t) ,  c.push_front(t) и c.insert(t, p) , где p — позиция итератора, заданная адаптеру вставки
*it, ++it, it++ Эти операторы существуют, но ничего не делают с итератором it . Каждый оператор возвращает итератор it

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

• Адаптер back_inserter (см. раздел 10.2.2) создает итератор, использующий функцию push_back().

• Адаптер front_inserter создает итератор, использующий функцию push_front().

• Адаптер inserter создает итератор, использующий функцию insert(). Кроме имени контейнера, адаптеру inserter передают второй аргумент: итератор, указывающий позицию, перед которой должна начаться вставка.

Адаптер front_inserter можно использовать, только если у контейнера есть функция push_front(). Аналогично адаптер back_inserter можно использовать, только если у контейнера есть функция push_back().

Важно понимать, что при вызове адаптера inserter(с, iter) возвращается итератор, который при использовании вставляет элементы перед элементом, первоначально обозначенным итератором iter. Таким образом, если it — итератор, созданный адаптером inserter, то присвоение *it = val; ведет себя, как следующий код:

it = c.insert(it, val); // it указывает на недавно добавленный элемент

++it; // инкремент it, чтобы он указывал на тот же элемент,

//  что и прежде

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

list lst = {1,2,3,4};

list lst2, lst3; // пустой список

// после завершения копирования, lst2 содержит 4 3 2 1

copy(lst.cbegin(), lst.cend(), front_inserter(lst2));

// после завершения копирования, lst3 содержит 1 2 3 4

copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

Когда происходит вызов front_inserter(c), возвращается итератор вставки, который последовательно вызывает функцию push_front(). По мере вставки каждого элемента он становится новым первым элементом контейнера с. Следовательно, адаптер front_inserter возвращает итератор, который полностью изменяет порядок последовательности, в которую осуществляется вставка; адаптеры inserter и back_inserter так не поступают.

Упражнения раздела 10.4.1

Упражнение 10.26. Объясните различия между тремя итераторами вставки.

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

Упражнение 10.28. Скопируйте вектор, содержащий значения от 1 до 9, в три других контейнера. Используйте адаптеры inserter, back_inserter и front_inserter соответственно для добавления элементов в эти контейнеры. Предскажите вид результирующей последовательности в зависимости от вида адаптера вставки и проверьте свои предсказания на написанной программе.

 

10.4.2. Потоковые итераторы

Хотя типы iostream не относятся к контейнерам, есть итераторы, применимые к объектам типов ввода-вывода (см. раздел 8.1). Итератор istream_iterator (табл. 10.3) читает входной поток, а итератор ostream_iterator (табл. 10.4) пишет в поток вывода. Эти итераторы рассматривают свой поток как последовательность элементов определенного типа. Используя потоковый итератор, можно применять обобщенные алгоритмы для чтения или записи данных в объекты потоков.

Таблица 10.3. Операторы итератора istream_iterator

istream_iterator<T> in(is); in читает значения типа T из входного потока is
istream_iterator<T> end; Итератор после конца для итератора  istream_iterator , читающего значения типа Т
in1 == in2 in1 != in2 in1 и in2 должны читать одинаковый тип. Они равны, если оба они конечные или оба связаны с тем же входным потоком
*in Возвращает значение, прочитанное из потока
in->mem Синоним для (*in).mem
++in , in++ Читает следующее значение из входного потока, используя оператор >> для типа элемента. Как обычно, префиксная версия возвращает ссылку на итератор после инкремента. Постфиксная версия возвращает прежнее значение

Использование итератора istream_iterator

Когда создается потоковый итератор, необходимо определить тип объектов, которые итератор будет читать или записывать. Итератор istream_iterator использует оператор >> для чтения из потока. Поэтому тип, читаемый итератором istream_iterator, должен определять оператор ввода. При создании итератор istream_iterator следует связать с потоком. В качестве альтернативы итератор можно инициализировать значением по умолчанию. В результате будет создан итератор, который можно использовать как значение после конца.

istream_iterator int_it(cin); // читает целые числа из cin

istream_iterator int_eof; // конечное значение итератора

ifstream in("afile");

istream_iterator str_it(in); // читает строки из "afile"

Для примера используем итератор istream_iterator для чтения со стандартного устройства ввода в вектор:

istream_iterator in_iter(cin); // читает целые числа из cin

istream_iterator eof;          // "конечный" итератор istream

while (in_iter != eof)              // пока есть что читать

 // постфиксный инкремент читает поток и возвращает прежнее значение

 // итератора. Обращение к значению этого итератора предоставляет

 // предыдущее значение, прочитанное из потока

 vec.push_back(*in_iter++);

Этот цикл читает целые числа из потока cin, сохраняя прочитанное в вектор vec. На каждой итерации цикл проверяет, не совпадает ли итератор in_iter со значением eof. Этот итератор был определен как пустой итератор istream_iterator, который используется как конечный итератор. Связанный с потоком итератор равен конечному итератору, только если связанный с ним поток достиг конца файла или произошла ошибка ввода-вывода.

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

Особенно полезно то, что эту программу можно переписать так:

istream_iterator in_iter(cin), eof; // читает целые числа из cin

vector vec(in_iter, eof);           // создает вектор vec из

                                         // диапазона итераторов

Здесь вектор vec создается из пары итераторов, которые обозначают диапазон элементов. Это итераторы istream_iterator, следовательно, диапазон получается при чтении связанного потока. Этот конструктор читает поток cin, пока он не встретит конец файла, или ввод, тип которого отличается от int. Прочитанные элементы используются для создания вектора vec.

Использование потоковых итераторов с алгоритмами

Поскольку алгоритмы используют функции итераторов, а потоковые итераторы поддерживают по крайней мере некоторые функции итератора, потоковые итераторы можно использовать с некоторыми из алгоритмов. Какие именно алгоритмы применимы с потоковыми итераторами, рассматривается в разделе 10.5.1. В качестве примера рассмотрим вызов функции accumulate() с парой итераторов istream_iterators:

istream_iterator in (cin), eof;

cout << accumulate(in, eof, 0) << endl;

Этот вызов создаст сумму значений, прочитанных со стандартного устройства ввода. Если ввод в этой программе будет таким:

23 109 45 89 6 34 12 90 34 23 56 23 8 89 23

то результат будет 664.

Итераторы istream_iterator позволяют использовать ленивое вычисление

То, что итератор istream_iterator связан с потоком, еще не гарантирует, что он начнет читать поток немедленно. Некоторые реализации разрешают задержать чтение потока, пока итератор не будет использован. Гарантированно, что поток будет прочитан перед первым обращением к значению итератора. Для большинства программ не имеет никакого значения, будет ли чтение немедленным или отсроченным. Но если создается итератор istream_iterator, который удаляется без использования, или если необходима синхронизация чтения того же потока из двух разных объектов, то тогда придется позаботиться и о моменте чтения.

Использование итератора ostream_iterator

Итератор ostream_iterator может быть определен для любого типа, у которого есть оператор вывода (оператор <<). При создании итератора ostream_iterator можно (необязательно) предоставить второй аргумент, определяющий символьную строку, выводимую после каждого элемента. Это должна быть символьная строка в стиле С (т.е. строковый литерал или указатель на массив с нулевым символом в конце). Итератор ostream_iterator следует связать с определенным потоком. Не бывает пустого итератора ostream_iterator или такового после конца.

Таблица 10.4. Операторы итератора ostream_iterator

ostream iterator<T> out(os); out пишет значения типа T в поток вывода os
ostream_iterator<T> out(os, d); out пишет значения типа T , сопровождаемые d в поток вывода os . d указывает на символьный массив с нулевым символом в конце
out = val Записывает val в поток вывода, с которым связан out, используя оператор << . Тип val должен быть совместим с типом, который можно писать в out
*out , ++out , out++ Эти операторы существуют, но ничего не делают с out . Каждый оператор возвращает итератор out

Итератор ostream_iterator можно использовать для записи последовательности значений:

ostream_iterator out_iter(cout, " ");

for (auto e : vec)

 *out_iter++ = e; // это присвоение запишет элемент в cout

cout << endl;

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

Следует заметить, что при присвоении итератору out_iter можно пропустить обращение к значению и инкремент. Таким образом, этот цикл можно переписать так:

for (auto е : vec)

 out_iter = е; // это присвоение запишет элемент в cout

cout << endl;

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

Чтобы не сочинять цикл самостоятельно, можно легко ввести элементы в вектор vec при помощи алгоритма copy():

copy(vec.begin(), vec.end(), out_iter);

cout << endl;

Использование потоковых итераторов с типами класса

Итератор istream_iterator можно создать для любого типа, у которого есть оператор ввода (>>). Точно так же итератор ostream_iterator можно определить для любого типа, обладающего оператором вывода (<<). Поскольку у класса Sales_item есть оба оператора (ввода и вывода), итераторы ввода-вывода вполне можно использовать, чтобы переписать программу книжного магазина из раздела 1.6:

istream_iterator item_iter(cin), eof;

ostream_iterator out_iter(cout, "\n");

// сохранить первую транзакцию в sum и читать следующую запись

Sales_item sum = *item_iter++;

while (item_iter != eof) {

 // если текущая транзакция (хранимая в item_iter) имеет тот же ISBN

 if (item_iter->isbn() == sum.isbn())

  sum += *item_iter++; // добавить ее к sum и читать следующую

                       // транзакцию

 else {

  out_iter = sum;      // вывести текущую сумму

  sum = *item_iter++;  // читать следующую транзакцию

 }

}

out_iter = sum; // не забыть вывести последний набор записей

Эта программа использует итератор item_iter для чтения транзакций Sales_item из потока cin. Она использует итератор out_iter для записи полученной суммы в поток cout, сопровождая каждый вывод символом новой строки. Определив итераторы, используем итератор item_iter для инициализации переменной sum значением первой транзакции:

// сохранить первую транзакцию в sum и читать следующую запись

Sales_item sum = *item_iter++;

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

Цикл while выполняется до тех пор, пока поток cin не встретит конец файла. В цикле while осуществляется проверка, не относится ли содержимое переменной sum и только что прочитанная запись к той же книге. Если это так, то только что прочитанный объект класса Sales_item добавляется в переменную sum. Если ISBN отличаются, переменная sum присваивается итератору out_iter, который выводит текущее значение переменной sum, сопровождаемое символом новой строки. После вывода суммы для предыдущей книги переменной sum присваивается копия последней прочитанной транзакции и осуществляется инкремент итератора для чтения следующей транзакции. Цикл продолжается до конца файла или ошибки чтения. Перед завершением следует вывести значения по последней книге во вводе.

Упражнения раздела 10.4.2

Упражнение 10.29. Напишите программу, использующую потоковые итераторы для чтения текстового файла в вектор строк.

Упражнение 10.30. Используйте потоковые итераторы, а также функции sort() и copy() для чтения последовательности целых чисел со стандартного устройства ввода, их сортировки и последующего вывода на стандартное устройство вывода.

Упражнение 10.31. Измените программу из предыдущего упражнения так, чтобы она выводила только уникальные элементы. Программа должна использовать алгоритм unique_copy() (см. раздел 10.4.1).

Упражнение 10.32. Перепишите программу книжного магазина из раздела 1.6. Используйте вектор для хранения транзакции и различные алгоритмы для обработки. Используйте алгоритм sort() с собственной функцией compareIsbn() из раздела 10.3.1 для упорядочивания транзакций, а затем используйте алгоритмы find() и accumulate() для вычисления суммы.

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

 

10.4.3. Реверсивные итераторы

Реверсивный итератор (reverse iterator) перебирает контейнер в обратном направлении, т.е. от последнего элемента к первому. Реверсивный итератор инвертирует смысл инкремента (и декремента): оператор ++it переводит реверсивный итератор на предыдущий элемент, а оператор --it — на следующий.

Реверсивные итераторы есть у всех контейнеров, кроме forward_list. Для получения реверсивного итератора используют функции-члены rbegin(), rend(), crbegin() и crend(). Они возвращают реверсивные итераторы на последний элемент в контейнере и на "следующий" (т.е. предыдущий) перед началом контейнера. Подобно обычным итераторам, существуют константные и неконстантные реверсивные итераторы.

Взаимное положение этих четырех итераторов на гипотетическом векторе vec представлено на рис. 10.1.

Рис. 10.1. Взаимное положение итераторов, возвращаемых функциями begin()/cend() и rbegin()/crend()

Рассмотрим, например, следующий цикл, выводящий элементы вектора vec в обратном порядке:

vector vec = {0,1,2,3,4,5,6,7,8,9};

// реверсивный итератор вектора (от конца к началу)

for (auto r_iter = vec.crbegin(); // связывает r_iter с последним

                                  // элементом

     r_iter != vec.crend();       // crend ссылается на 1 элемент

                                  // перед 1-м

     ++r_iter) // декремент итератора на один элемент

 cout << *r_iter << endl; // выводит 9, 8, 7, ... 0

Хотя смысл оператора декремента реверсивного итератора может показаться неправильным, этот оператор позволяет применять для обработки контейнера стандартные алгоритмы. Например, передав функции sort() два реверсивных итератора, вектор можно отсортировать в порядке убывания.

sort(vec.begin(), vec.end()); // сортирует вектор vec

                              // в "нормальном" порядке

// обратная сортировка: самый маленький элемент располагается

// в конце вектора vec

sort(vec.rbegin(), vec.rend());

Реверсивным итераторам необходим оператор декремента

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

#magnify.png Отношения между реверсивными и другими итераторами

Предположим, что существует объект line класса string( строка ) , содержащий разделяемый запятыми список слов. Используя функцию find(), можно отобразить, например, первое слово строки line:

// найти первый элемент в списке, разделенном запятыми

auto comma = find(line.cbegin(), line.cend(), ',');

cout << string(line.cbegin(), comma) << endl;

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

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

// найти последний элемент в списке, разделенном запятыми

auto rcomma = find(line.crbegin(), line.crend(), ',');

Поскольку функции find() в качестве аргументов передаются результаты выполнения функций crbegin() и crend(), поиск начинается с последнего символа в строке line в обратном порядке. По завершении поиска, если запятая найдена, итератор rcomma будет указывать на последнюю запятую в строке, т.е. первую запятую с конца. Если запятой нет, итератор rcomma будет равен итератору, возвращаемому функцией line.crend().

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

// ошибка: создаст слово в обратном порядке

cout << string(line.crbegin(), rcomma) << endl;

Например, если введена строка " FIRST,MIDDLE,LAST " , будет получен результат "TSAL"!

Эта проблема проиллюстрирована на рис. 10.2. Здесь реверсивные итераторы используются для перебора строки в обратном порядке. Поэтому оператор вывода выводит строку line назад, начиная от crbegin(). Вместо этого следует выводить строку от rcomma и до конца. Но итератор rcomma нельзя использовать непосредственно, так как это реверсивный итератор, обеспечивающий перебор от конца к началу. Поэтому необходимо преобразовать его назад в обычный итератор, перебирающий строку вперед. Для преобразования итератора rcomma можно применить функцию-член base(), которой обладает каждый реверсивный итератор.

// ok: получить прямой итератор и читать до конца строки

cout << string(rcomma.base(), line.cend()) << endl;

С учетом того, что введены те же данные, в результате отобразится слово "LAST", как и ожидалось.

Рис. 20.2. Отношения между реверсивными и обычными итераторами

Объекты, представленные на рис. 10.2, наглядно иллюстрируют взаимоотношения между обычными и реверсивными итераторами. Например, итераторы rcomma и возвращаемый функцией rcomma.base() указывают на разные элементы, так же как и возвращаемые функциями line.crbegin() и line.cend(). Эти различия вполне обоснованны: они позволяют гарантировать возможность одинаковой обработки диапазона элементов при перемещении как вперед, так и назад.

С технической точки зрения отношения между обычными и реверсивными итераторами приспособлены к свойствам диапазона, включающего левый элемент (см. раздел 9.2.1). Дело в том, что [line.crbegin(), rcomma) и [rcomma.base(), line.cend()) ссылаются на тот же элемент в строке line. Для этого rcomma и rcomma.base() должны возвращать соседние позиции, а не ту же позицию, как функции crbegin() и cend().

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

Упражнения раздела 10.4.3

Упражнение 10.34. Используйте итератор reverse_iterator для вывода содержимого вектора в обратном порядке.

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

Упражнение 10.36. Используйте функцию find() для поиска в списке целых чисел последнего элемента со значением 0.

Упражнение 10.37. С учетом того, что вектор содержит 10 элементов, скопируйте в список диапазон его элементов от позиции 3 до позиции 7 в обратном порядке.

 

10.5. Структура обобщенных алгоритмов

 

Фундаментальное свойство любого алгоритма — это список функциональных возможностей, которые он требует от своего итератора (итераторов). Некоторые алгоритмы, например find(), требуют только возможности получить доступ к элементу через итератор, прирастить итератор и сравнить два итератора на равенство. Другие, такие как sort(), требуют возможности читать, писать и произвольно обращаться к элементам. По своим функциональным возможностям, обязательным для алгоритмов, итераторы группируются в пять категорий (iterator categories), перечисленных в табл. 10.5. Каждый алгоритм определяет, итератор какого вида следует предоставить для каждого из его параметров.

Таблица 10.5. Категории итераторов

Итератор ввода Обеспечивает чтение, но не запись; поддерживает только инкремент
Итератор вывода Обеспечивает запись, но не чтение; поддерживает только инкремент
Прямой итератор Обеспечивает чтение и запись; поддерживает только инкремент
Двунаправленный итератор Обеспечивает чтение и запись; поддерживает инкремент и декремент
Итератор произвольного доступа Обеспечивает чтение и запись; поддерживает все арифметические операции итераторов

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

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

 

10.5.1. Пять категорий итераторов

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

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

Стандарт определяет минимальную категорию для каждого параметра итератора обобщенных и числовых алгоритмов. Например, алгоритм find(), реализующий перебор последовательности только для чтения и в одном направлении, минимально требует только итератор ввода. Алгоритму replace() требуется два итератора, являющихся, по крайней мере, прямыми итераторами. Аналогично алгоритм replace_copy() требует прямые итераторы для своих первых двух итераторов. Его третий итератор, представляющий назначение, должен, по крайней мере, быть итератором вывода и т.д. Итератор для каждого параметра должен обладать не меньшим набором параметров, чем предусмотренный минимум. Передача итератора с меньшими возможностями недопустима.

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

Категории итераторов

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

• Операторы равенства и неравенства (==, !=), используемые для сравнения двух итераторов.

• Префиксный и постфиксный инкременты (++), используемые для перемещения итератора.

• Оператор обращения к значению (*), позволяющий прочитать элемент. Оператор обращения к значению может быть применен только к операнду, расположенному справа от оператора присвоения.

• Оператор стрелки (->), равнозначный выражению (*it).member. То есть обращение к значению итератора и доступ к члену класса объекта.

Итераторы ввода могут быть использованы только последовательно. Гарантирована допустимость инкремента *it++, но приращение итератора ввода может сделать недопустимыми все другие итераторы в потоке. В результате нет никакой гарантии того, что можно сохранить состояние итератора ввода и исследовать элемент с его помощью. Поэтому итераторы ввода можно использовать только для однопроходных алгоритмов. Алгоритмам find() и accumulate() требуются итераторы ввода, а итератор istream_iterator — имеет тип итератора ввода.

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

• Префиксный и постфиксный инкременты (++), используемые для перемещения итератора.

• Оператор обращения к значению (*) может быть применен только к операнду, расположенному слева от оператора присвоения. Присвоение при обращении к значению итератора вывода позволяет осуществить запись в элемент.

Значение итератору вывода можно присвоить только однажды. Подобно итераторам ввода, итераторы вывода можно использовать только для однопроходных алгоритмов. Итераторы, используемые как итераторы назначения, обычно являются итераторами вывода. Например, третий параметр алгоритма copy() является итератором вывода. Итератор ostream_iterator имеет тип итератора вывода.

• Прямой итератор (forward iterator) позволяет читать и записывать данные в последовательность. Они перемещаются по последовательности только в одном направлении. Прямые итераторы поддерживают все операции итераторов ввода и вывода. Кроме того, они позволяют читать и записывать значение в тот же элемент несколько раз. Поэтому сохраненное состояние прямого итератора можно использовать. Следовательно, алгоритмы, использующие прямые итераторы, могут осуществить несколько проходов через последовательность. Алгоритму replace() требуется прямой итератор; итераторы контейнера forward_list являются прямыми итераторами.

• Двунаправленный итератор (bidirectional iterator) позволяет читать и записывать данные в последовательность в обоих направлениях. Кроме всех функциональных возможностей прямого итератора, двунаправленный итератор поддерживает также префиксный и постфиксный декременты (--). Алгоритму reverse() требуется двунаправленный итератор. Все библиотечные контейнеры, кроме forward_list, предоставляют итераторы, соответствующие требованиям для двунаправленного итератора.

• Итератор прямого доступа (random-access iterator) обеспечивает доступ к любой позиции последовательности в любой момент. Эти итераторы обладают всеми функциональными возможностями двунаправленных итераторов. Кроме того, они поддерживают операции, приведенные в табл. 3.7.

  • Операторы сравнения <, <=, > и >=, позволяющие сравнить относительные позиции двух итераторов.

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

  • Оператор вычитания (-), применяемый к двум итераторам, позволяет получить дистанцию между двумя итераторами.

  • Оператор индексирования (iter[n]), равнозначный выражению *(iter + n).

Итератор прямого доступа необходим алгоритму sort(). Итераторы контейнеров array, deque, string и vector являются итераторами прямого доступа, подобно указателям массива.

Упражнения раздела 10.5.1

Упражнение 10.38. Перечислите пять категорий итераторов и операции, которые каждый из них поддерживает.

Упражнение 10.39. Итератором какой категории обладает список? А вектор?

Упражнение 10.40. Итераторы какой категории нужны алгоритму copy()? А алгоритмам reverse() и unique()?

 

10.5.2. Параметрическая схема алгоритмов

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

алг (beg, end, другие параметры );

алг (beg, end, dest, другие параметры );

алг (beg, end, beg2, другие параметры );

алг (beg, end, beg2, end2, другие параметры );

где алг — это имя алгоритма, а параметры beg и end обозначают исходный диапазон элементов, с которыми работает алгоритм. Хотя почти все алгоритмы получают исходный диапазон, присутствие других параметров зависит от выполняемых действий. Как правило, остальные параметры, dest, beg2 и end2, также являются итераторами. Кроме них, некоторые алгоритмы получают дополнительные параметры, не являющиеся итераторами.

Алгоритмы с одним итератором назначения

Параметр dest (destination — назначение) — это итератор, обозначающий получателя, используемого для хранения результата. Алгоритмы подразумевают, что способны безопасно записать в последовательность назначения столько элементов, сколько необходимо.

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

Если dest является итератором контейнера, алгоритм записывает свой результат в уже существующие элементы контейнера. Как правило, итератор dest связан с итератором вставки (см. раздел 10.4.1) или итератором ostream_iterator (см. раздел 10.4.2). Итератор вставки добавляет новые элементы в контейнер, гарантируя, таким образом, достаточную емкость. Итератор ostream_iterator осуществляет запись в поток вывода, а следовательно, тоже не создает никаких проблем независимо от количества записываемых элементов.

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

Алгоритмы, получающие один параметр (beg2) или два параметра (beg2 и end2), используют эти итераторы для обозначения второго исходного диапазона. Как правило, для выполнения необходимых действий эти алгоритмы используют элементы второго диапазона вместе с элементами исходного.

Когда алгоритм получает параметры beg2 и end2, эти итераторы обозначают весь второй диапазон. Такой алгоритм получает два полностью определенных диапазона: исходный диапазон, обозначенный итераторами [beg, end), а также второй, исходный диапазон, обозначенный итераторами [beg2, end2).

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

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

 

10.5.3. Соглашения об именовании алгоритмов

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

Некоторые алгоритмы используют перегруженные версии для передачи предиката

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

unique(beg, end); // использует для сравнения элементов оператор ==

unique(beg, end, comp); // использует для сравнения элементов

                        // предикат comp

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

Алгоритмы с версиями _if

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

find(beg, end, val); // найти первый экземпляр val в исходном диапазоне

find_if(beg, end, pred); // найти первый экземпляр, для

                         // которого pred возвращает true

Оба алгоритма находят в исходном диапазоне первый экземпляр заданного элемента. Алгоритм find() ищет указанное значение, а алгоритм find_if() — значение, для которого предикат pred возвратит значение, отличное от нуля.

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

Различия между копирующими и не копирующими версиями

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

reverse(beg, end); // обратить порядок элементов в исходном диапазоне

reverse_copy(beg, end, dest); // скопировать элементы по назначению в

                              // обратном порядке

Некоторые алгоритмы предоставляют и версии _copy, и _if. Эти версии получают и итератор назначения, и предикат:

// удаляет нечетные элементы из v1

remove_if(v1.begin(), v1.end(),

          [](int i) { return i % 2; });

// копирует только четные элементы из v1 в v2; v1 неизменен

remove_copy_if(v1.begin(), v1.end(), back_inserter(v2),

               [](int i) { return i % 2; });

Для определения нечетности элемента оба вызова используют лямбда-выражение (см. раздел 10.3.2). В первом случае нечетные элементы удаляются из самой исходной последовательности. Во втором не нечетные (четные) элементы копируются из исходного диапазона в вектор v2.

Упражнения раздела 10.5.3

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

replace(beg, end, old_val, new_val);

replace_if(beg, end, pred, new_val);

replace_copy(beg, end, dest, old_val, new_val);

replace_copy_if(beg, end, dest, pred, new_val);

 

10.6. Алгоритмы, специфические для контейнеров

В отличие от других контейнеров, контейнеры list и forward_list определяют несколько алгоритмов в качестве членов. В частности, тип list определяют собственные версии алгоритмов sort(), merge(), remove(), reverse() и unique(). Обобщенная версия алгоритма sort() требует итераторов произвольного доступа. В результате она не может использоваться с контейнерами list и forward_list, поскольку эти типы предоставляют двунаправленные и прямые итераторы соответственно.

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

Эти специфические для списка функции приведены в табл. 10.6. В ней нет обобщенных алгоритмов, которые получают соответствующие итераторы и выполняются одинаково эффективно как для других контейнеров, так и для контейнеров list и forward_list.

Предпочтительней использовать алгоритмы-члены классов list и forward_list, а не их обобщенные версии.

Таблица 10.6. Алгоритмы-члены классов list и forward_list  

Эти функции возвращают void .
lst.merge(lst2) lst.merge(lst2, comp) Объединяет элементы списков lst2 и lst . Оба списка должны быть отсортированы. Элементы из списка lst2 удаляются, и после объединения список lst2 оказывается пустым. Возвращает тип void . В первой версии используется оператор < , а во второй — указанная функция сравнения
lst.remove(val) lst.remove_if(pred) При помощи функции lst.erase() удаляет каждый элемент, значение которого равно переданному значению, или для которого указанный унарный предикат возвращает значение, отличное от нуля
lst.reverse() Меняет порядок элементов списка lst на обратный
lst.sort() lst.sort(comp) Сортирует элементы списка lst , используя оператор < или другой заданный оператор сравнения
lst.unique() lst.unique(pred) При помощи функции lst.erase() удаляет расположенные рядом элементы с одинаковыми значениями. Вторая версия использует заданный бинарный предикат

#books.png Алгоритм-член splice()

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

Таблица 10.7. Аргументы алгоритма-члена splice() классов list и forward_list

lst.splice( аргументы ) или flst.splice_after( аргументы )
(p, lst2) p — итератор на элемент списка lst или итератор перед элементом списка flst . Перемещает все элементы из списка lst2 в список lst непосредственно перед позицией p или непосредственно после в списке flst . Удаляет элементы из списка lst2 . Список lst2 должен иметь тот же тип, что и lst (или flst ), и не может быть тем же списком
(p, lst2, p2) p2 — допустимый итератор в списке lst2 . Перемещает элемент, обозначенный итератором p2 , в список lst или элемент после обозначенного итератором p2 в списке flst . Список lst2 может быть тем же списком, что и lst или flst
(p, lst2, b, е) b и е обозначают допустимый диапазон в списке lst2 . Перемещает элементы в заданный диапазон из списка lst2 . Списки lst2 и lst (или flst ) могут быть тем же списком, но итератор p не должен указывать на элемент в заданном диапазоне

Специфические для списка функции, изменяющие контейнер

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

Аналогично алгоритмы merge() и splice() деструктивны к своим аргументам. Например, обобщенная версия функции merge() запишет объединенную последовательность по заданному итератору назначения; две исходных последовательности останутся неизменны. Специфическая для списка функция merge() разрушит заданный список — элементы будут удаляться из списка аргумента по мере их объединения в объект, для которого был вызван аргумент merge(). После объединения элементы из обоих списков продолжают существовать, но принадлежат уже одному списку.

Упражнения раздела 10.6

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

 

Резюме

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

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

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

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

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

Контейнеры forward_list и list определяют собственные версии некоторых из обобщенных алгоритмов. В отличие от обобщенных алгоритмов, эти специфические версии изменяют переданные им списки.

 

Термины

Адаптерback_inserter. Адаптер итератора, который, получив ссылку на контейнер, создает итератор вставки, использующий функцию push_back() для добавления элементов в указанный контейнер.

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

Адаптерinserter. Адаптер итератора, который, получив итератор и ссылку на контейнер, создает итератор вставки, используемый функцией insert() для добавления элементов непосредственно перед элементом, указанным данным итератором.

Бинарный предикат (binary predicate). Предикат с двумя параметрами.

Вызываемый объект (callable object). Объект, способный быть левым операндом оператора вызова. К вызываемым объектам относятся указатели на функции, лямбда-выражения и объекты классов, определяющих перегруженные версии оператора вызова.

Двунаправленный итератор (bidirectional iterator). Поддерживает те же операции, что и прямые итераторы, плюс способность использовать оператор -- для перемещения по последовательности назад.

Итераторistream_iterator. Потоковый итератор, обеспечивающий чтение из потока ввода.

Итераторostream_iterator. Потоковый итератор, обеспечивающий запись в поток вывода.

Итератор ввода (input iterator). Итератор, позволяющий читать, но не записывать элементы.

Итератор вставки (insert iterator). Итератор, использующий функции контейнера для добавления элементов в данный контейнер.

Итератор вывода (output iterator). Итератор, позволяющий записывать, но не обязательно читать элементы.

Итератор перемещения (move iterator). Итератор, позволяющий перемещать элементы, а не копировать их. Итераторы перемещения рассматриваются в главе 13.

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

Категории итераторов (iterator categories). Концептуальная организация итераторов на основании поддерживаемых ими операций. Категории итераторов составляют иерархию, в которой более мощные итераторы предоставляют те же операции, что и менее мощные. Пока итератор обеспечивает, по крайней мере, достаточный уровень операций, он вполне применим. Например, некоторым алгоритмам требуются только итераторы ввода. Такие алгоритмы могут быть применены к любому другому итератору, который обладает возможностями, не ниже, чем у итератора вывода. Алгоритмы, которым необходимы итераторы прямого доступа, применимы только для тех итераторов, которые поддерживают операции прямого доступа.

Лямбда-выражение (lambda expression). Вызываемый блок кода. Лямбды немного похожи на безымянные встраиваемые функции. Они начинается со списка захвата, позволяющего лямбда-выражению получать доступ к переменным в содержащей функции. Подобно функции, имеет список параметров (возможно пустой), тип возвращаемого значения и тело функции. У лямбда-выражения может отсутствовать тип возвращаемого значения. Если тело функции представляет собой одиночный оператор return, тип возвращаемого значения выводится из типа возвращаемого объекта. В противном случае типом пропущенного возвращаемого значения по умолчанию принимается void.

Обобщенный алгоритм (generic algorithm). Алгоритм, не зависящий от типа контейнера.

Потоковый итератор (stream iterator). Итератор, который может быть связан с потоком.

Предикат (predicate). Функция, которая возвращает значение типа bool (логическое) или допускающее преобразование в него. Зачастую используется обобщенными алгоритмами для проверки элементов. Используемые библиотекой предикаты являются либо унарными (получающими один аргумент), либо бинарными (получающими два аргумента).

Прямой итератор (forward iterator). Итератор, позволяющий читать и записывать элементы, но не поддерживающий оператор --.

Реверсивный итератор (reverse iterator). Итератор, позволяющий перемещаться по последовательности назад. У этих итераторов операторы ++ и -- имеют противоположный смысл.

Список захвата (capture list). Часть лямбда-выражения, определяющая переменные из окружающего контекста, к которым может обращаться лямбда-выражение.

Унарный предикат (unary predicate). Предикат с одним параметром.

Функцияbind(). Библиотечная функция, связывающая один или несколько аргументов с вызываемым выражением. Функция bind() определена в заголовке functional.

Функцияcref(). Библиотечная функция, возвращающая копируемый объект, содержащий ссылку на константный объект типа, не допускающего копирования.

Функцияref(). Библиотечная функция, создающая копируемый объект из ссылки на объект типа, не допускающего копирования.

 

Глава 11

Ассоциативные контейнеры

 

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

Хотя поведение ассоциативных контейнеров по большей части одинаково, у последовательных контейнеров оно отличается и зависит от способа использования ключей.

Ассоциативные контейнеры (associative container) обеспечивают быстрый поиск и предоставление элементов по ключу. Двумя первичными типами ассоциативных контейнеров являются map (карта) и set (набор). Элементами контейнера map являются пары ключ-значение (key-value pair): ключ выступает в роли индекса, а значение представляет собой хранимые в контейнере данные. Контейнер set содержит только ключи и предоставляет эффективные способы запроса на проверку наличия определенного ключа. Набор можно было бы использовать для хранения слов, которые следует проигнорировать при некой обработке текста. Карту можно использовать для словаря: слово было бы ключом, а его определение — значением.

Библиотека предоставляет восемь ассоциативных контейнеров (табл. 11.1), которые различаются по трем факторам: (1) они являются набором (set) или картой map; (2) они требуют уникальных ключей или допускает их совпадение; (3) они хранят элементы упорядочено или нет. В именах контейнеров, допускающих совпадение ключей, присутствует слово multi; имена контейнеров, не упорядочивающих хранимые ключи начинаются со слова unordered. Следовательно, unordered_multi_set — это набор, не требующий уникальных ключей и хранящий элементы неупорядоченными, в то время как set — это набор с уникальными ключами, которые хранятся упорядочено. Для организации своих элементов неупорядоченные контейнеры используют хеш-функцию. Подробно хеш-функции рассматриваются в разделе 11.4.

Таблица 11.1. Типы ассоциативных контейнеров

Элементы упорядочиваются по ключу
map Ассоциативный массив, хранящий пары ключ-значение
set Контейнер, в котором ключ является значением
multimap Карта, допускающая совпадение ключей
multiset Набор, допускающий совпадение ключей
Неупорядоченные коллекции
unordered_map Карта, организованная по хеш-функции
unordered_set Набор, организованный по хеш-функции
unordered_multimap Хешированная карта; ключи могут повторяться
unordered multiset Хешированный набор; ключи могут повторяться

Типы map и multimap определены в заголовке map; классы set и multiset — в заголовке set; неупорядоченные версии контейнеров определены в заголовках unordered_map и unordered_set соответственно.

 

11.1. Использование ассоциативных контейнеров

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

Карта (тип map) — это коллекция пар ключ-значение. Например, каждая пара может содержать имя человека как ключ и номер его телефона как значение. О такой структуре данных говорят, что она "сопоставляет имена с номерами телефонов". Тип map зачастую называют ассоциативным массивом (associative array). Ассоциативный массив похож на обычный массив, но его индексы не обязаны быть целыми числами. Значения в карте находят по ключу, а не по их позиции. В карте имен и номеров телефонов имя человека использовалось бы как индекс для поиска номера телефона этого человека.

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

Использование контейнера map

Классическим примером применения ассоциативного массива является программа подсчета слов:

// подсчитать, сколько раз каждое слово встречается во вводе

map word_count; // пустая карта строк и чисел

string word;

while (cin >> word)

 ++word_count[word]; // получить и прирастить счетчик слов

for (const auto &w : word_count) // для каждого элемента карты

 // отобразить результаты

 cout << w.first << " occurs " << w.second

      << ((w.second >1) ? " times" : " time") << endl;

Эта программа читает ввод и сообщает, сколько раз встречается каждое слово.

Подобно последовательным контейнерам, ассоциативные контейнеры являются шаблонами (см. раздел 3.3). Чтобы определить карту, следует указать типы ключа и значения. В этой программе карта хранит элементы, ключи которых имеют тип string, а значения — тип size_t (см. раздел 3.5.2). При индексации карты word_count строка используется как индекс, а возвращаемый счетчик типа size_t связан с этой строкой.

Цикл while читает слова со стандартного устройства ввода по одному за раз. Он использует каждое слово для индексирования карты word_count. Если слова еще нет в карте, оператор индексирования создает новый элемент, ключом которого будет слово, а значением 0. Независимо от того, должен ли быть создан элемент, его значение увеличивается.

Как только весь ввод прочитан, серийный оператор for (см. раздел 3.2.3) перебирает карту выводя каждое слово и соответствующий счетчик.

При получении элемента из карты возвращается объект типа pair (пара), рассматриваемого в разделе 11.2.3 (стр. 545). Если не вдаваться в подробности, то pair — это шаблон типа, который содержит две открытые переменные-члена по имени first (первый) и second (второй). У используемых картой пар член first является ключом, a second — соответствующим значением. Таким образом, оператор вывода должен отобразить каждое слово и связанный с ним счетчик.

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

Although occurs 1 time

Before occurs 1 time

an occurs 1 time

and occurs 1 time

...

Использование контейнера set

Логичным усовершенствованием создаваемой программы будет игнорирование таких распространенных слов, как "the", "and", "or" и т.д. Для хранения игнорируемых слов будет использован набор, а подсчитываться будут только те слова, которые отсутствуют в этом наборе:

// подсчитать, сколько раз каждое слово встречается во вводе

map word_count; // пустая карта строк и чисел

set exclude = {"The", "But", "And", "Or", "An", "A",

                       "the", "but", "and", "or", "an", "a"};

string word;

while (cin >> word)

 // подсчитать только не исключенные слова

 if (exclude.find(word) == exclude.end())

  ++word_count[word]; // получить и прирастить счетчик слов

Подобно другим контейнерам, set является шаблоном. Чтобы определить набор, следует указать тип его элементов, которым в данном случае будет string. Подобно последовательным контейнерам, для элементов ассоциативного контейнера применима списочная инициализация (см. раздел 3.3.6). Набор exclude будет содержать 12 слов, которые следует игнорировать.

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

// подсчитать только не исключенные слова

if (exclude.find(word) == exclude.end())

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

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

Although occurs 1 time

Before occurs 1 time

are occurs 1 time

as occurs 1 time

...

Упражнения раздела 11.1

Упражнение 11.1. Опишите различия между картой и вектором.

Упражнение 11.2. Приведите пример того, когда наиболее полезен контейнер list, vector, deque, map и set.

Упражнение 11.3. Напишите собственную версию программы подсчета слов.

Упражнение 11.4. Усовершенствуйте свою программу так, чтобы игнорировать регистр и пунктуацию. Т.е. слова "example" и "Example", например, должны увеличить тот же счетчик.

 

11.2. Обзор ассоциативных контейнеров

 

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

Кроме функций, общих для всех контейнеров, ассоциативные контейнеры предоставляют некоторые функции (табл. 11.7) и псевдонимы типов (табл. 11.3), которых нет у последовательных контейнеров. Помимо этого, неупорядоченные контейнеры предоставляют функции настройки производительности их хеша, которые рассматриваются в разделе 11.4.

Итераторы ассоциативных контейнеров двунаправлены (см. раздел 10.5.1).

 

11.2.1. Определение ассоциативного контейнера

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

map word_count; // пустая карта

// списочная инициализация 

set exclude = {"the", "but", "and", "or", "an", "a",

                       "The", "But", "And", "Or", "An", "A"};

// три элемента; authors сопоставляет фамилию с именем

map authors = { {"Joyce", "James"},

                                {"Austen", "Jane"},

                                {"Dickens", "Charles"} };

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

При инициализации карты следует предоставить и ключ, и значение. Каждая пара ключ-значение заключается в фигурные скобки, { ключ , значение } , означая, что вместе элементы формируют единый элемент карты. Первый элемент каждой пары — это ключ, второй — значение. Таким образом, карта authors сопоставляет фамилии с именами и инициализируется тремя элементами.

Инициализация контейнеров multimap и multiset

Ключи в контейнерах map и set должны быть уникальными; с каждым ключом может быть сопоставлен только один элемент. У контейнеров multimap и multiset такого ограничения нет; вполне допустимо несколько элементов с тем же ключом. Например, у использованной для подсчета слов карты должен быть только один элемент, содержащий некое слово. С другой стороны, у словаря может быть несколько определений того же слова.

Следующий пример иллюстрирует различия между контейнерами с уникальными ключами и таковыми с не уникальными ключами. Сначала необходимо создать вектор целых чисел ivec на 20 элементов: две копии каждого из целых чисел от 0 до 9 включительно. Этот вектор будет использован для инициализации контейнеров set и multiset:

// определить вектор из 20 элементов, содержащий две копии каждого

// числа от 0 до 9

vector ivec;

for (vector::size_type i = 0; i != 10; ++i) {

 ivec.push_back(i);

 ivec.push_back(i); // сдублировать каждое число

}

// iset содержит уникальные элементы ivec;

// miset содержит все 20 элементов

set iset(ivec.cbegin(), ivec.cend());

multiset miset(ivec.cbegin(), ivec.cend());

cout << ivec.size() << endl;  // выводит 20

cout << iset.size() << endl;  // выводит 10

cout << miset.size() << endl; // выводит 20

Хотя набор iset был инициализирован значениями всего контейнера ivec, он содержит только десять элементов: по одному для каждого уникального элемента вектора ivec. С другой стороны, контейнер miset содержит 20 элементов, сколько и вектор ivec.

Упражнения раздела 11.2.1

Упражнение 11.5. Объясните различие между картой и набором. Когда имеет смысл использовать один, а когда другой?

Упражнение 11.6. Объясните различия между набором и списком. Когда имеет смысл использовать один, а когда другой?

Упражнение 11.7. Определите карту, ключ которой является фамилией семьи, а значение — вектором имен детей. Напишите код, способный добавлять новые семьи и новых детей в существующие семьи.

Упражнение 11.8. Напишите программу, которая хранит исключенные слова в векторе, а не в наборе. Каковы преимущества использования набора?

 

11.2.2. Требования к типу ключа

Ассоциативные контейнеры налагают ограничения на тип ключа. Требования для ключей неупорядоченных контейнеров рассматриваются в разделе 11.4. У упорядоченных контейнеров (map, multimap, set и multiset) тип ключа должен определять способ сравнения элементов. По умолчанию для сравнения ключей библиотека использует оператор < типа ключа. В наборах тип ключа соответствует типу элемента; в картах тип ключа — тип первого элемента пары. Таким образом, типом ключа карты word_count (см. раздел 11.1) будет string. Аналогично типом ключа набора exclude также будет string.

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

Типы ключей упорядоченных контейнеров

Подобно тому, как собственный оператор сравнения можно предоставить алгоритму (см. раздел 10.3), собственный оператор можно также предоставить для использования вместо оператора < ключей. Заданный оператор должен обеспечить строгое сравнение (strict weak ordering) для типа ключа. Строгое сравнение можно считать оператором "меньше", хотя наша функция могла бы использовать более сложную процедуру. Однако самостоятельно определяемая функция сравнения должна обладать свойствами, описанными ниже.

• Два ключа не могут быть "меньше" друг друга; если ключ k1 "меньше", чем k2, то k2 никогда не должен быть "меньше", чем k1.

• Если ключ k1 "меньше", чем k2, и ключ k2 "меньше", чем k3, то ключ k1 должен быть "меньше", чем k3.

• Если есть два ключа и ни один из них не "меньше" другого, то эти ключи "эквивалентны". Если ключ k1 "эквивалентен" ключу k2 и ключ k2 "эквивалентен" ключу k3, то ключ k1 должен быть "эквивалентен" ключу k3.

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

На практике очень важно, чтобы тип, определяющий "обычный" оператор <, был применим в качестве ключа.

Использование функции сравнения для типа ключа

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

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

Например, невозможно непосредственно определить контейнер multiset объектов класса Sales_data, поскольку класс Sales_data не имеет оператора <. Но для этого можно использовать функцию compareIsbn() из упражнений раздела 10.3.1. Эта функция обеспечивает строгое сравнение на основании ISBN двух объектов класса Sales_data. Функция compareIsbn() должна выглядеть примерно так:

bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {

 return lhs.isbn() < rhs.isbn();

}

Чтобы использовать собственный оператор, следует определить контейнер multiset с двумя типами: типом ключа Sales_data и типом сравнения, являющимся типом указателя на функцию (см. раздел 6.7), способным указывать на функцию compareIsbn(). Когда определяют объекты этого типа, предоставляют указатель на функцию, которую предстоит использовать. В данном случае предоставляется указатель на функцию compareIsbn():

// в программе может быть несколько транзакций с тем же ISBN

// элементы bookstore упорядочены по ISBN

multiset

 bookstore(compareIsbn);

Здесь для определения типа оператора используется спецификатор decltype. При использовании спецификатора decltype для получения указателя на функцию следует добавить символ * для обозначения использования указателя на заданный тип функции (см. раздел 6.7). Инициализацию bookstore осуществляет функция compareIsbn(). Это означает, что при добавлении элементов в bookstore они будут упорядочены при вызове функции compareIsbn(). Таким образом, элементы bookstore будут упорядочены по их члену ISBN. Аргумент конструктора можно записать как compareIsbn, вместо &compareIsbn, поскольку при использовании имени функции оно автоматически преобразуется в указатель, если это нужно (см. раздел 6.7). С тем же результатом можно написать &compareIsbn.

Упражнения раздела 11.2.2

Упражнение 11.9. Определите карту, которая ассоциирует слова со списком номеров строк, в которых оно встречается.

Упражнение 11.10. Можно ли определить карту для типов vector::iterator и int? А для типов list::iterator и int? Если нет, то почему?

Упражнение 11.11. Переопределите bookstore, не используя спецификатор decltype.

 

11.2.3. Тип

pair

Прежде чем перейти к рассмотрению действий с ассоциативными контейнерами, имеет смысл ознакомиться с библиотечным типом pair (пара), определенным в заголовке utility.

Объект типа pair хранит две переменные-члена. Подобно контейнерам, тип pair является шаблоном, позволяющим создавать конкретные типы. При создании пары следует предоставить имена двух типов, которые будут типами ее двух переменных-членов. Совпадать эти типы вовсе не обязаны.

pair anon;       // содержит две строки

pair word_count; // содержит строку и целое число

pair> line;  // содержит строку и vector

При создании объекта пары без указания инициализирующих значений используются стандартные конструкторы типов его переменных-членов. Таким образом, пара anon содержит две пустые строки, а пара line — пустую строку и пустой вектор целых чисел. Значением переменной-члена типа int в паре word_count будет 0, а его переменная-член типа string окажется инициализирована пустой строкой.

Можно также предоставить инициализаторы для каждого члена пары:

pair author{"James", "Joyce"};

Этот код создает пару по имени author, инициализированную значениями "James" и "Joyce".

В отличие от других библиотечных типов, переменные-члены класса pair являются открытыми (см. раздел 7.2). Эти члены — first (первый) и second (второй) соответственно. К ним можно обращаться непосредственно, используя обычный точечный оператор (см. раздел 1.5.2), как, например, было сделано в операторе вывода программы подсчета слов в разделе 11.1:

// отобразить результаты

cout << w.first << " occurs " << w.second

     << ((w.second > 1) ? " times" : " time") << endl;

где w — ссылка на элемент карты. Элементами карты являются пары. В данном операторе выводится переменная-член first элемента, являющаяся ключом, затем переменная-член second элемента, являющаяся счетчиком. Библиотека определяет весьма ограниченный набор операций с парами, который приведен в табл. 11.2.

Таблица 11.2. Операции с парами

pair<T1, T2> p; p — пара с переменными-членами типов T1 и T2 , инициализированными значением по умолчанию (см. раздел 3.3.1)
pair<T1, T2> р(v1, v2); p — пара с переменными-членами типов T1 и T2 , инициализированными значениями v1 и v2 соответственно
pair<T1, T2> р = {v1, v2}; Эквивалент p(v1, v2 )
make_pair(v1, v2) Возвращает пару, инициализированную значениями v1 и v2 . Тип пары выводится из типов значений v1 и v2
p.first Возвращает открытую переменную-член first пары p
p.second Возвращает открытую переменную-член second пары p
p1 опсравн p2 Операторы сравнения ( < , > , <= , >= ). Сравнение осуществляется подобно упорядочиванию в словаре, т.е. оператор < возвращает значение true в случае, если p1.first < p2.first или !(p2.first < p1.first) && p1.second < p2.second
p1 == p2 , p1 != p2 Две пары равны, если их первый и второй члены соответственно равны. При сравнении используется оператор == хранимых элементов

Функция для создания объектов типа pair

Предположим, некая функция должна возвратить значение типа pair. По новому стандарту возможна списочная инициализация возвращаемого значения (см. раздел 6.3.2):

pair

process(vector &v) {

 // обработка v

 if (!v.empty())

  return {v.back(), v.back().size()}; // списочная инициализация

 else

  return pair(); // возвращаемое значение создано явно

}

Если вектор v не пуст, возвращается пара, состоящая из последней строки в векторе v и размера этой строки. В противном случае явно создается и возвращается пустая пара.

В прежних версиях языка С++ нельзя было использовать инициализаторы в скобках для возвращения типа, подобного pair. Вместо этого можно было написать оба оператора return как явно созданное возвращаемое значение:

if (!v.empty())

 return pair(v.back(), v.back().size());

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

if (!v.empty())

 return make_pair(v.back(), v.back().size());

Упражнения раздела 11.2.3

Упражнение 11.12. Напишите программу, читающую последовательность строк и целых чисел, сохраняя каждую прочитанную пару в объекте класса pair. Сохраните пары в векторе.

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

Упражнение 11.14. Дополните карту фамилий семей и их детей, написанную для упражнения в разделе 11.2.1, вектором пар, содержащих имя ребенка и день его рождения.

 

11.3. Работа с ассоциативными контейнерами

 

В дополнение к типам, перечисленным в табл. 9.2 (стр. 423), ассоциативные контейнеры определяют типы, перечисленные в табл. 11.3. Они представляют типы ключа и значения контейнера.

Таблица 11.3. Псевдонимы дополнительных типов ассоциативных контейнеров

key_type Тип ключа контейнера
mapped_type Тип, ассоциированный с каждым ключом; только для типа map
value_type Для наборов то же, что и key_type . Для карт — pair<const key_type, mapped type>

Для контейнеров типа set типы key_type и value_type совпадают; содержащиеся в наборе данные являются ключами. Элементами карты являются пары ключ-значение. Таким образом, каждый ее элемент — объект класса pair, содержащий ключ и связанное с ним значение. Поскольку ключ элемента изменить нельзя, ключевая часть этих пар константна:

set::value_type v1;       // v1 - string

set::key_type v2;         // v2 - string

map::value_type v3;  // v3 - pair

map::key_type v4;    // v4 - string

map::mapped_type v5; // v5 - int

Подобно последовательным контейнерам (см. раздел 9.2.2), для доступа к члену класса, например типа map::key_type, используется оператор области видимости.

Тип mapped_type определен только для типов карт (unordered_map, unordered_multimap, multimap и map).

 

11.3.1. Итераторы ассоциативных контейнеров

При обращении к значению итератора возвращается ссылка на значение типа value_type контейнера. В случае карты типом value_type является пара, переменная-член first которой содержит константный ключ, а переменная-член second — значение:

// получить итератор на элемент контейнера word_count

auto map_it = word_count.begin();

// *map_it - ссылка на объект типа pair

cout << map_it->first;         // отобразить ключ элемента

cout << " " << map_it->second; // отобразить значение элемента

map_it->first = "new key";     // ошибка: ключ является константой

++map_it->second; // ok: значение можно изменить, используя итератор

Не следует забывать, что типом value_type карты является pair и что можно изменять ее значение, но не ключ.

Итераторы наборов константны

Хотя типы наборов определяют типы iterator и const_iterator, оба типа итераторов предоставляют доступ к элементам в наборе только для чтения. Подобно тому, как нельзя изменить ключевую часть элемента карты, ключи в наборе также константны. Итератор набора можно использовать только для чтения, но не для записи значения элемента:

set iset = {0,1,2,3,4,5,6,7,8,9};

set::iterator set_it = iset.begin();

if (set_it != iset.end()) {

 *set_it = 42;            // ошибка: ключи набора только для чтения

 cout << *set_it << endl; // ok: позволяет читать ключ

}

Перебор ассоциативного контейнера

Типы map и set поддерживают все функции begin() и end() из табл. 9.2. Как обычно, эти функции можно использовать для получения итераторов, позволяющих перебрать контейнер. Например, цикл вывода результатов программы подсчета слов из раздела 11.1 можно переписать следующим образом:

// получить итератор на первый элемент

auto map_it = word_count.cbegin();

// сравнить текущий итератор с итератором после конца

while (map_it != word_count.cend()) {

 // обратиться к значению итератора, чтобы отобразить

 // пару ключ-значение элемента

 cout << map_it->first << " occurs "

      << map_it->second << " times" << endl;

 ++map_it; // прирастить итератор, чтобы перейти на следующий элемент

}

Условие цикла while и инкремент итератора в теле цикла такие же как в программах вывода содержимого векторов или строк. Итератор map_it инициализирован позицией первого элемента контейнера word_count. Пока итератор не равен значению, возвращенному функцией end(), возвращается текущий элемент, а затем происходит приращение итератора. Оператор вывода обращается к значению итератора map_it для получения членов пары, оставаясь в остальном тем же, что и в первоначальной программе.

Вывод этой программы имеет алфавитный порядок. При использовании итераторов для перебора контейнеров map, multimap, set и multiset они возвращают элементы в порядке возрастания ключа.

Ассоциативные контейнеры и алгоритмы

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

Ассоциативные контейнеры применимы с теми алгоритмами, которые только читают элементы. Однако большинство этих алгоритмов осуществляет поиск в последовательности. Поскольку поиск элементов в ассоциативном контейнере осуществляется быстро (по ключу), как правило, не имеет смысла использовать для них обобщенный алгоритм поиска. Например, как будет продемонстрировано в разделе 11.3.5, ассоциативные контейнеры определяют функцию-член find(), позволяющую непосредственно выбрать элемент с заданным ключом. Для поиска элемента можно использовать обобщенный алгоритм find(), но он осуществляет последовательный поиск. Поэтому намного быстрее использовать функцию-член find() класса контейнера, чем вызывать обобщенную версию.

На практике, если это вообще происходит, ассоциативный контейнер используется с алгоритмами в качестве исходной последовательности или последовательности назначения. Например, обобщенный алгоритм copy() можно использовать для копирования элементов ассоциативного контейнера в другую последовательность. Точно так же адаптер inserter можно использовать для связи итератора вставки (см. раздел 10.4.1) с ассоциативным контейнером. Адаптер inserter позволяет использовать ассоциативный контейнер как место назначения для другого алгоритма.

Упражнения раздела 11.3.1

Упражнение 11.15. Каковы типы mapped_type, key_type и value_type карты, переменные-члены пар которой имеют типы int и vector?

Упражнение 11.16. Используя итератор карты, напишите выражение, присваивающее значение элементу.

Упражнение 11.17. С учетом того, что с — контейнер multiset строк, a v — вектор строк, объясните следующие вызовы. Укажите, допустим ли каждый из них:

copy(v.begin(), v.end(), inserter(с, c.end()));

copy(v.begin(), v.end(), back inserter(c));

copy(c.begin(), c.end(), inserter(v, v.end()));

copy(c.begin(), c.end(), back inserter(v));

Упражнение 11.18. Перепишите определение типа map_it из цикла в данном разделы, не используя ключевое слово auto или decltype.

Упражнение 11.19. Определите переменную, инициализированную вызовом функции begin() контейнера multiset по имени bookstore из раздела 11.2.2. Определите тип переменной, не используя ключевое слово auto или decltype.

 

11.3.2. Добавление элементов

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

vector ivec = {2,4,6,8,2,4,6,8};    // ivec содержит

                                         //  восемь элементов

set set2;                           // пустой набор

set2.insert(ivec.cbegin(), ivec.cend()); // set2 имеет четыре элемента

set2.insert({1,3,5,7,1,3,5,7}); // теперь set2 имеет восемь элементов

Таблица 11.4. Функция insert() ассоциативного контейнера

с.insert(v) с.emplace( args ) v — объект типа value_type ; аргументы args используются при создании элемента. Элементы карты и набора вставляются (или создаются), только если элемента с данным ключом еще нет в контейнере с . Возвращает пару, содержащую итератор на элемент с заданным ключом и логическое значение, указывающее, был ли вставлен элемент. У контейнеров multimap и multiset осуществляется вставка (или создание) заданного элемента и возвращение итератора на новый элемент
с.insert(b, e) с.insert(il) Итераторы b и е обозначают диапазон значений типа с::value_type ; il — заключенный в скобки список таких значений. Возвращает void . У карты и набора вставляются элементы с ключами, которых еще нет в контейнере с . У контейнеров multimap и multiset вставляются все элементы диапазона
c.insert(p, v) с.emplace(p,  args ) Подобны функциям insert(v) и emplace( args ) , но используют итератор p как подсказку для начала поиска места хранения нового элемента. Возвращает итератор на элемент с заданным ключом

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

Добавление элементов в карту

При вставке в карту следует помнить, что типом элемента является pair. Зачастую объекта pair, подлежащего вставке, нет. В этом случае пара создается в списке аргументов функции insert():

// четыре способа добавления слова в word_count

word_count.insert({word, 1});

word_count.insert(make_pair(word, 1));

word_count.insert(pair(word, 1));

word_count.insert(map::value_type(word, 1));

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

map::value_type(s, 1)

Он создает новый объект пары соответствующего типа для вставки в карту.

Проверка значения, возвращаемого функцией insert()

Значение, возвращенное функцией insert() (или emplace()), зависит от типа контейнера и параметров. Для контейнеров с уникальными ключами есть версии функций insert() и emplace(), которые добавляют один элемент и возвращают пару, сообщающую об успехе вставки. Первая переменная-член пары — итератор на элемент с заданным ключом; второй — логическое значение, указывающее на успех вставки элемента. Если такой ключ уже был в контейнере, то функция insert() не делает ничего, а логическая часть возвращаемого значения содержит false. Если такой ключ отсутствовал, то логическая часть содержит значение true.

Для примера перепишем программу подсчета слов с использованием функции insert():

// более корректный способ подсчета слов во вводе

map word_count; // пустая карта строк и чисел

string word;

while (cin >> word) {

 // вставляет элемент с ключом, равным слову, и значением 1;

 // если слово уже есть в word_count, insert() не делает ничего

 auto ret = word_count.insert({word, 1});

 if (!ret.second) // слово уже было в word_count

  ++ret.first->second; // приращение счетчика

}

Для каждой строки word осуществляется попытка вставки со значением 1. Если слово уже находится в карте, ничего не происходит. В частности, связанный со словом счетчик остается неизменным. Если слова еще нет в карте, оно добавляется, а значение его счетчика устанавливается в 1.

Оператор if проверяет логическую часть возвращаемого значения. Если это значение false, то вставка не произошла. Следовательно, слово уже было в карте word_count, поэтому следует увеличить значение связанного с ним счетчика.

Еще раз о синтаксисе

Оператор приращения счетчика в этой версии программы подсчета слов трудно понять. Разобрать это выражение будет существенно проще, если сначала расставить скобки в соответствии с приоритетом (см. раздел 4.1.2) операторов:

++((ret.first)->second); // эквивалентное выражение

Рассмотрим это выражение поэтапно.

• ret — пара, содержащая значение, возвращаемое функцией insert().

• ret.first — первая переменная-член пары, на которую указывает итератор карты, с данным ключом.

• ret.first-> — обращение к значению итератора, позволяющее получить этот элемент. Элементы карты также являются парами.

• ret.first->second — та часть пары элемента карты, которая является значением.

• ++ret.first->second — инкремент этого значения.

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

Для читателей, использующих устаревший компилятор или код, предшествующий новому стандарту, объявление и инициализация пары ret также не совсем очевидны:

pair::iterator, bool> ret =

 word_count.insert(make_pair(word, 1));

Здесь определяется пара, вторая переменная-член которой имеет тип bool. Понять тип первой переменной-члена этой пары немного труднее. Это тип итератора, определенный типом map.

Добавление элементов в контейнеры multiset и multimap

Работа программы подсчета слов зависит от того факта, что каждый ключ может присутствовать только однажды. Таким образом, с любым словом будет связан только один счетчик. Но иногда необходима возможность добавить дополнительные элементы с тем же ключом. Например, могло бы понадобиться сопоставить авторов с названиями написанных ими книг. В данном случае для каждого автора могло бы быть несколько записей, поэтому будет использован контейнер multimap, а не map. Поскольку ключи контейнеров multi не должны быть уникальным, функция insert() для них всегда вставляет элемент:

multimap authors;

// добавляет первый элемент с ключом Barth, John

authors.insert({"Barth, John", "Sot-Weed Factor"});

// ok: добавляет второй элемент с ключом Barth, John

authors.insert({"Barth, John", "Lost in the Funhouse"});

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

Упражнения раздела 11.3.2

Упражнение 11.20. Перепишите программу подсчета слов из раздела 11.1 так, чтобы использовать функцию insert() вместо индексации. Какая версия программы по-вашему проще? Объясните почему.

Упражнение 11.21. С учетом того, что word_count является картой типов string и size_t, а также того, что word имеет тип string, объясните следующий цикл:

while (cin >> word)

 ++word_count.insert({word, 0}).first->second;

Упражнение 11.22. С учетом, что map>, напишите типы, используемые как аргументы, и возвращаемое значение версии функции insert(), вставляющей один элемент.

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

 

11.3.3. Удаление элементов

Ассоциативные контейнеры определяют три версии функции erase(), описанные в табл. 11.5. Подобно последовательным контейнерам, можно удалить один элемент или диапазон элементов, передав функции erase() итератор или пару итераторов. Эти версии функции erase() подобны соответствующим функциям последовательных контейнеров: указанный элемент (элементы) удаляется и возвращается тип void.

Таблица 11.5. Удаление элементов ассоциативного контейнера

c.erase(k) Удаляет из карты с элемент с ключом k . Возвращает значение типа size_type , указывающее количество удаленных элементов
c.erase(p) Удаляет из карты с элемент, обозначенный итератором p . Итератор p должен относиться к фактически существующему элементу карты с , он не может быть равен итератору, возвращаемому функцией c.end() . Возвращает итератор на элемент после позиции p или c.end() , если итератор p обозначает последний элемент контейнера с
c.erase(b, е) Удаляет элементы в диапазоне, обозначенном парой итераторов b и е . Возвращает итератор е

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

// удалить по ключу, возвратить количество удаленных элементов

if (word_count.erase(removal_word))

 cout << "ok: " << removal_word << " removed\n";

else

 cout << "oops: " << removal_word << " not found!\n";

Для контейнеров с уникальными ключами функция erase() всегда возвращает нуль или единицу. Если возвращается значение нуль, значит, удаляемого элемента не было в контейнере.

Для контейнеров с не уникальными ключами функция erase() возвращает количество удаленных элементов и может быть больше единицы:

auto cnt = authors.erase("Barth, John");

Если authors — это контейнер multimap, созданный в разделе 11.3.2, то переменная cnt будет содержать значение 2.

 

11.3.4. Индексация карт

Контейнеры map и unordered_map предоставляют оператор индексирования и соответствующую функцию at() (см. раздел 9.3.2), представленные в табл. 11.6. Типы контейнеров set не поддерживают индексацию, поскольку в наборе нет никакого "значения", связанного с ключом. Элементы сами являются ключами, поэтому операция "доступа к значению, связанному с ключом", бессмысленна. Нельзя индексировать контейнер multimap или unordered_multimap, поскольку с заданным ключом может быть ассоциировано несколько значений.

Таблица 11.6. Операторы индексирования контейнеров map и unordered_map

c[k] Возвращает элемент с ключом k ; если ключа k нет в контейнере с , добавляется новый элемент, инициализированный значением с ключом k
c.at(k) Проверяет наличие элемента с ключом k ; если его нет в контейнере с , передает исключение out_of_range (см. раздел 5.6)

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

Рассмотрим следующий код:

map word_count; // пустая карта

// вставить инициализированный значением по умолчанию элемент

// с ключом Anna; а затем установить для него значение 1

word_count["Anna"] = 1;

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

• В контейнере word_count происходит поиск элемента с ключом Anna. Элемент не найден.

• В контейнер word_count добавляется новая пара ключ-значение. Ключ (константная строка) содержит текст Anna. Значение инициализируется по умолчанию, в данном случае нулем.

• Вновь созданному элементу присваивается значение 1.

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

Индексация карт существенно отличается от индексации массивов или векторов: использование отсутствующего ключа приводит к добавлению элемента с таким ключом в карту.

Использование значения, возвращенного оператором индексирования

Иной способ индексирования карт, отличающий его от других использованных ранее операторов индексирования, влияет на тип возвращаемого значения. Обычно тип, возвращенный в результате обращения к значению итератора, и тип, возвращенный оператором индексирования, совпадают. У карт все не так: при индексировании возвращается объект типа mapped_type, а при обращении к значению итератора карты — объект типа value_type (см. раздел 11.3).

Общим у всех операторов индексирования является то, что они возвращают l-значение (см. раздел 4.1.1). Поскольку возвращается l-значение, возможно чтение и запись в элемент:

cout << word_count["Anna"]; // получить элемент по индексу Anna;

                            // выводит 1

++word_count["Anna"];       // получить элемент и добавить к нему 1

cout << word_count["Anna"]; // получить элемент и вывести его;

                            // выводит 2

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

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

Упражнения раздела 11.3.4

Упражнение 11.24. Что делает следующая программа?

map m;

m[0] = 1;

Упражнение 11.25. Сравните следующую программу с предыдущей:

vector v;

v[0] = 1;

Упражнение 11.26. Какой тип применяется при индексировании карты? Какой тип возвращает оператор индексирования? Приведите конкретный пример, т.е. создайте карту, используйте типы, которые применимы для ее индексирования, а затем выявите типы, которые будет возвращать оператор индексирования.

 

11.3.5. Доступ к элементам

Ассоциативные контейнеры предоставляют различные способы поиска заданных элементов, описанные в табл. 11.7. Используемый способ зависит от решаемой задачи. Если нужно лишь выяснить, находится ли некий элемент в контейнере, то, вероятно, лучше использовать функцию find(). Для контейнеров, способных содержать только уникальные ключи, вероятно, не имеет значения, используется ли функция find() или count(). Но для контейнеров с не уникальными ключами функция count() выполняет больше работы: если элемент присутствует, ей все еще нужно подсчитать количество элементов с тем же ключом. Если знать количество не обязательно, лучше использовать функцию find():

set iset = {0,1,2,3,4,5,6,7,8,9};

iset.find(1);   // возвращает итератор на элемент с ключом == 1

iset.find(11);  // возвращает итератор == iset.end()

iset.count(1);  // возвращает 1

iset.count(11); // возвращает 0

Таблица 11.7. Функции поиска элементов в ассоциативном контейнере

Функции lower_bound() и upper_bound() неприменимы для неупорядоченных контейнеров. Оператор индексирования и функция at() применимы только для тех контейнеров map и unordered_map , которые не являются константами.
c.find(k) Возвращает итератор на (первый) элемент с ключом k или итератор после конца, если такого элемента нет в контейнере
c.count(k) Возвращает количество элементов с ключом k . Для контейнеров с уникальными ключами результат всегда нуль или единица
c.lower_bound(k) Возвращает итератор на первый элемент, значение ключа которого не меньше, чем k
c.upper_bound(k) Возвращает итератор на первый элемент, значение ключа которого больше, чем k
c.equal_range(k) Возвращает пару итераторов, обозначающих элементы с ключом k . Если такового элемента нет, значение обеих переменных-членов равно c.end()

Использование функции find() вместо индексирования карт

Для контейнеров map и unordered_map оператор индексирования представляет простейший способ поиска значения. Но, как уже упоминалось, у оператора индексирование есть серьезный побочный эффект: если искомого ключа еще нет в карте, индексирование добавляет элемент с таким ключом. Насколько правильно такое поведение, зависит от обстоятельств. Программа подсчета слов полагалась на тот факт, что использование несуществующего ключа при индексировании приводило к вставке элемента с этим ключом и значением 0.

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

if (word_count.find("foobar") == word_count.end())

 cout << "foobar is not in the map" << endl;

Поиск элементов в контейнерах multimap и multiset

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

Предположим, например, что, имея карту авторов и их книг, следует вывести все книги некоего автора. Эту задачу можно решить тремя способами. Самый очевидный из них — использовать функции find() и count():

string search_item("Alain de Botton"); // искомый автор

auto entries = authors.count(search_item); // количество записей

auto iter = authors.find(search_item); // первая запись для этого

                                       // автора

// перебор записей данного автора

while (entries) {

 cout << iter->second << endl; // вывод каждого заглавия

 ++iter;    // переход к следующему заглавию

 --entries; // отследить количество выведенных записей

}

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

Гарантируется, что перебор контейнера multimap или multiset возвратит все элементы с заданным ключом.

Другое решение на основании итератора

Задачу можно решить иначе, используя функции lower_bound() и upper_bound(). Каждая из них получает ключ и возвращает итератор. Если ключ найден в контейнере, функция lower_bound() возвратит итератор на первый экземпляр элемента с этим ключом, а итератор, возвращенный функцией upper_bound(), указывает на следующий элемент после последнего экземпляра с заданным ключом. Если таковой элемент в контейнере multimap отсутствует, то функции lower_bound() и upper_bound() возвратят одинаковые итераторы на позицию, в которой мог бы находиться такой ключ согласно принятому порядку. Таким образом, вызов функций lower_bound() и upper_bound() для того же ключа возвращает диапазон итераторов (см. раздел 9.2.1), обозначающий все элементы с тем же ключом.

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

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

Используя эти функции, можно переписать программу следующим образом:

// определения authors и search_item как прежде

// итераторы beg и end обозначают диапазон элементов данного автора

for (auto beg = authors.lower_bound(search_item),

          end = authors.upper_bound(search_item);

     beg != end; ++beg)

 cout << beg->second << endl; // вывод каждого заглавия

Эта программа делает то же, что и предыдущая, использовавшая функции count() и find(), но более непосредственно. Вызов функции lower_bound() устанавливает итератор beg так, чтобы он указывал на первый элемент, соответствующий search_item, если он есть. Если его нет, то итератор beg укажет на первый элемент с ключом, большим, чем search_item, который может оказаться итератором после конца. Вызов функции upper_bound() присвоит итератору end позицию элемента непосредственно после последнего элемента с заданным ключом. Эти функции ничего не говорят о том, присутствует ли данный ключ в контейнере. Важный момент заключается в том, что возвращаемые значения формируют диапазон итераторов (см. раздел 9.2.1).

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

Если элементы с заданным ключом есть, то итератор beg укажет на первый такой элемент. Приращение итератора beg позволит перебрать элементы с этим ключом. Равенство итератора beg итератору end свидетельствует о завершении перебора всех элементов с этим ключом.

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

Если функции lower_bound() и upper_bound() возвращают тот же итератор, то заданного ключа в контейнере нет.

Функция equal_range()

Последний способ решения этой задачи самый простой из всех: вместо функций upper_bound() и lower_bound() можно вызвать функцию equal_range().

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

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

// определения authors и search_item, как прежде

// pos содержит итераторы, обозначающие диапазон элементов

// с заданным ключом

for (auto pos = authors.equal_range(search_item);

     pos.first != pos.second; ++pos.first)

 cout << pos.first->second << endl; // вывод каждого заглавия

Эта программа очень похожа на предыдущую, где использовались функции upper_bound() и lower_bound(). Для хранения диапазона итераторов вместо локальных переменных beg и end используется пара, возвращенная функцией equal_range(). Переменная-член first этой пары содержит тот же итератор, который возвратила бы функция lower_bound(), а переменная-член second — итератор, который возвратила бы функция upper_bound(). Таким образом, в этой программе значение pos.first эквивалентно значению beg, a pos.second — значению end.

Упражнения раздела 11.3.5

Упражнение 11.27. Для решения каких видов задач используется функция count()? Когда вместо нее можно использовать функцию find()?

Упражнение 11.28. Определите и инициализируйте переменную, содержащую результат вызова функции find() для карты строк и векторов целых чисел.

Упражнение 11.29. Что возвращают функции upper_bound(), lower_bound() и equal_range(), когда им передается ключ, отсутствующий в контейнере?

Упражнение 11.30. Объясните значение операнда pos.first->second, использованного в выражении вывода последней программы данного раздела.

Упражнение 11.31. Напишите программу, определяющую контейнер multimap авторов и их работ. Используйте функцию find() для поиска элемента и его удаления. Убедитесь в корректности работы программы, когда искомого элемента нет в карте.

Упражнение 11.32. Используя контейнер multimap из предыдущего упражнения, напишите программу вывода списка авторов и их работ в алфавитном порядке.

 

11.3.6. Карта преобразования слов

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

Вот содержимое файла преобразования слов.

brb be right back

k okay?

y why

r are

u you

pic picture

thk thanks!

l8r later

Подлежащий преобразованию текст таков:

where r u

y dont u send me a pic

k thk l8r

Программа должна создать следующий вывод:

where are you

why dont you send me a picture

okay? thanks! later

Программа преобразования слова

Решение подразумевает использование трех функций. Функция word_transform() будет осуществлять общую обработку. Потребуются два аргумента типа ifstream: первый будет связан с файлом преобразования слов, а второй — с текстовым файлом, который предстоит преобразовать. Функция buildMap() будет читать файл правил преобразования и создавать элемент карты для каждого слова и результата его преобразования. Функция transform() получит строку и, если она есть в карте, возвратит результат преобразования.

Давайте начнем с определения функции word_transform(). Важнейшие ее части — вызовы функций buildMap() и transform():

void word_transform(ifstream &map_file, ifstream &input) {

 auto trans_map = buildMap(map_file); // хранит преобразования

 string text; // содержит каждую строку из ввода

 while (getline(input, text)) { // читать строку из ввода

  istringstream stream(text); // читать каждое слово

  string word;

  bool firstword = true; // контролирует вывод пробела

  while (stream >> word) {

   if (firstword)

    firstword = false;

   else

    cout << " "; // вывод пробела между словами

   // transform() возвращает свой первый аргумент или

   // результат преобразования

   cout << transform(word, trans_map); // вывод результата

  }

  cout << endl; // обработка текущей строки ввода окончена

 }

}

Функция начинается вызовом функции buildMap(), создающим карту преобразования слов. Результат сохраняется в карте trans_map. Остальная часть функции обрабатывает входной файл. Цикл while использует функцию getline() для чтения входного файла по одной строке за раз. Построчно чтение осуществляется для того, чтобы строки вывода заканчивались там же, где и строки входного файла. Для получения слов каждой строки используется вложенный цикл while, использующий строковый поток istringstream (см. раздел 8.3) для обработки каждого слова текущей строки.

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

Создание карты преобразования

Функция buildMap() читает переданный ей файл и создает карту преобразований.

map buildMap(ifstream &map_file) {

 map trans_map; // хранит преобразования

 string key;   // слово для преобразования

 string value; // фраза, используемая вместо него

 // прочитать первое слово в ключ, а остальную часть строки в значение

 while (map_file >> key && getline(map_file, value))

  if (value.size() > 1) // проверить, есть ли преобразование

   trans_map[key] = value.substr(1); // убрать предваряющий

                                     // пробел

 else

  throw runtime_error("no rule for " + key);

 return trans_map;

}

Каждая строка файла map_file соответствует правилу. Каждое правило — это слово, сопровождаемое фразой, способной содержать несколько слов. Для чтения слов, преобразуемых в ключи, используется оператор >> и функция getline() для чтения остальной части строки в значение. Поскольку функция getline() не отбрасывает предваряющие пробелы (см. раздел 3.2.2), необходимо убрать пробел между словом и соответствующим ему правилом. Прежде чем сохранить преобразование, осуществляется проверка наличия в нем хотя бы одного символа. Если это так, то происходит вызов функции substr() (см. раздел 9.5.1), позволяющий устранить пробел, отделяющий фразу преобразования от соответствующего ему слова, и сохранить эту подстроку в карте trans_map.

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

Осуществление преобразования

Фактическое преобразование осуществляет функция transform(). Ее параметры — ссылки на преобразуемую строку и карту преобразования. Если переданная строка находится в карте, функция transform() возвращает соответствующую ей фразу преобразования. Если переданной строки в карте нет, функция transform() возвращает свой аргумент:

const string &

transform(const string &s, const map &m) {

 // фактическая работа карты; это основная часть программы

 auto map_it = m.find(s);

 // если слово есть в карте преобразования

 if (map it != m.cend())

  return map_it->second; // использовать замену слова

 else

  return s; // в противном случае возвратить исходное слово

}

Код начинается с вызова функции find(), позволяющего определить, находится ли данная строка в карте. Если это так, то функция find() возвращает итератор на соответствующий элемент. В противном случае функция find() возвращает итератор на элемент после конца. Если элемент найден, обращение к значению итератора возвращает пару, содержащую ключ и значение этого элемента (см. раздел 11.3). Функция возвращает значение переменной-члена second этой пары, являющееся преобразованной фразой, используемой вместо строки s.

Упражнения раздела 11.3.6

Упражнение 11.33. Реализуйте собственную версию программы преобразования слов.

Упражнение 11.34. Что будет, если в функции transform() вместо функции find() использовать оператор индексирования ?

Упражнение 11.35. Что будет (если будет) при таком изменении функции buildMap():

trans_map[key] = value.substr(1);

as trans_map.insert({key, value.substr(1)})?

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

 

11.4. Неупорядоченные контейнеры

Новый стандарт определяет четыре неупорядоченных ассоциативных контейнера (unordered container). Вместо оператора сравнения для организации своих элементов эти контейнеры используют хеш-функцию (hash function) и оператор == типа ключа. Неупорядоченный контейнер особенно полезен, когда имеющийся тип ключа не дает очевидных отношений для упорядочивания элементов. Эти контейнеры полезны также в приложениях, где цена упорядочивания элементов высока.

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

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

Использование неупорядоченного контейнера

Кроме функций управления хешированием, неупорядоченные контейнеры предоставляют те же функции (find(), insert() и т.д.), что и упорядоченные контейнеры. Это значит, что функции, использовавшиеся для контейнеров map и set, применимы также к контейнерам unordered_map и unordered_set. Аналогично неупорядоченные контейнеры имеют версии с не уникальными ключами.

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

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

// подсчет слов, но слова не в алфавитном порядке

unordered_map word_count;

string word;

while (cin >> word)

 ++word_count[word]; // получить и прирастить счетчик слов

for (const auto &w : word_count) // для каждого элемента карты

 // отобразить результаты

 cout << w.first << " occurs " << w.second

      << ((w.second >1) ? " times" : " time") << endl; 

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

containers occurs 1 time

use occurs 1 time

can occurs 1 time

examples occurs 1 time

...

Но вывод вряд ли будет в алфавитном порядке.

Управление ячейками

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

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

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

Таблица 11.8. Функции управления неупорядоченным контейнером

Взаимодействие с ячейками
с.bucket_count() Количество используемых ячеек
c.max_bucket_count() Наибольшее количество ячеек, которое может содержать данный контейнер
c.bucket_size(n) Количество элементов в ячейке n
c.bucket(k) Ячейка, в которой следует искать элементы с ключом k
Перебор ячеек
local_iterator Тип итератора, способный обращаться к элементам в ячейке
const_local_iterator Константная версия итератора ячейки
c.begin(n) , c.end(n) Итераторы на первый и следующий после последнего элементы ячейки n
c.cbegin(n) , c.cend(n) Возвращают итератор const_local_iterator
Политика хеша
c.load_factor() Среднее количество элементов на ячейку. Возвращает тип  float
c.max_load_factor() Средний размер ячейки, который пытается поддерживать контейнер c . Контейнер с добавляет ячейки, чтобы сохранить соотношение load_factor <= max_load_factor . Возвращает тип float
c.rehash(n) Реорганизует хранилище так, чтобы bucket_count >= n  и bucket_count > size/max_load_factor
c.reserve(n) Реорганизует контейнер c так, чтобы он мог содержать n элементов без вызова функции rehash()

Требования к типу ключа неупорядоченных контейнеров

По умолчанию для сравнения элементов неупорядоченные контейнеры используют оператор == типа ключа. Они также используют объект типа hash при создании хеш-кода для каждого элемента. Библиотека поставляет также версии шаблона хеша для встроенных типов, включая указатели. Она определяет также шаблон hash для некоторых из библиотечных типов, включая строки и интеллектуальные указатели, которые рассматривались в главе 12. Таким образом, можно непосредственно создать неупорядоченный контейнер, ключ которого имеет один из встроенных типов (включающий типы указателей) либо тип string или интеллектуального указателя.

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

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

size_t hasher(const Sales_data &sd) {

 return hash()(sd.isbn());

}

bool eqOp(const Sales_data &lhs, const Sales_data &rhs) {

 return lhs.isbn() == rhs.isbn();

}

Чтобы создать хеш-код для переменной-члена ISBN, функция hasher() использует объект библиотечного типа hash для типа string. Точно так же функция eqOp() сравнивает два объекта класса Sales_data, сравнивая их ISBN.

Эти функции можно также использовать для определения контейнера unordered_multiset следующим образом:

using SD_multiset = unordered_multiset

                     decltype(hasher)*, decltype(eqOp)*>;

// аргументы - размер ячейки, указатель на оператор равенства и

// хеш-функцию

SD_multiset bookstore(42, hasher, eqOp);

Чтобы упростить объявление bookstore, определим сначала псевдоним типа (см. раздел 2.5.1) для контейнера unordered_multiset, у хеша и оператора равенства которого есть те же типы, что и у функций hasher() и eqOp(). Используя этот тип, определим bookstore, передав указатели на функции, которые он должен использовать.

Если у класса есть собственный оператор ==, можно переопределить только хеш-функцию:

// использовать FooHash для создания хеш-кода;

// у Foo должен быть оператор ==

unordered_set fooSet(10, FooHash);

Упражнения раздела 11.4

Упражнение 11.37. Каковы преимущества неупорядоченного контейнера по сравнению с упорядоченной версией этого контейнера? Каковы преимущества упорядоченной версии?

Упражнение 11.38. Перепишите программы подсчета слов (см. раздел 11.1) и преобразования слов (см. раздел 11.3.6) так, чтобы использовать контейнер unordered_map.

 

Резюме

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

Существует восемь ассоциативных контейнеров со следующими свойствами.

• Карта хранит пары ключ-значение; набор хранит только ключи.

• Есть контейнеры с уникальными ключами и с не уникальными.

• Ключи могут храниться упорядоченными или нет.

Упорядоченные контейнеры используют функцию сравнения для упорядочивания элементов по ключу. По умолчанию для сравнения используется оператор < типа ключа. Неупорядоченные контейнеры используют для организации своих элементов оператор == типа ключа и объект типа hash.

Имена контейнеров с не уникальными ключами включают слово multi; а имена контейнеров, использующих хеширование, начинаются словом unordered. Контейнер set — это упорядоченная коллекция, каждый ключ которой уникален; контейнер unordered_multiset — это неупорядоченная коллекция, ключи которой могут повторяться.

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

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

 

Термины

Ассоциативный контейнер (associative container). Тип, содержащий коллекцию объектов и обеспечивающий эффективный поиск по ключу.

Ассоциативный массив (associative array). Массив, элементы которого проиндексированы по ключу, а не по позиции. Таким образом, массив сопоставляет (ассоциирует) ключ со значением.

Контейнерmap (карта). Ассоциативный контейнер, аналогичный ассоциативному массиву. Подобно типу vector, тип map является шаблоном класса. Но при создании карты необходимо указать два типа: тип ключа и тип связанного с ним значения. В контейнере map ключи уникальны, они не повторяются. Каждый ключ связан с определенным значением. Обращение к значению итератора карты возвращает объект типа pair, который содержит константный ключ и связанное (ассоциированное) с ним значение.

Контейнерmultimap. Ассоциативный контейнер, подобный контейнеру map, но способный содержать одинаковые ключи.

Контейнерmultiset. Ассоциативный контейнер, который содержит только ключи. В отличие от набора, способен содержать одинаковые ключи.

Контейнерset (набор). Ассоциативный контейнер, который содержит только ключи. Ключи в контейнере set не могут совпадать.

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

Контейнерunordered_multimap. Контейнер, элементы которого являются парами ключ-значение. Допустимо несколько элементов на ключ.

Контейнерunordered_multiset. Контейнер, хранящий ключи. Допустимо несколько элементов на ключ.

Контейнерunordered_set. Контейнер, хранящий ключи. Допустим только один элемент на ключ.

Неупорядоченный контейнер (unordered container). Ассоциативные контейнеры, использующие хеширование, а не сравнение ключей для хранения и доступа к элементам. Эффективность этих контейнеров зависит от качества хеш-функции.

Оператор *. Оператор обращения к значению, примененный к итератору контейнера map, set, multimap или multiset, возвращает объект типа value_type. Обратите внимание на то, что типом value_type контейнера map и multimap является пара (pair).

Оператор []. Оператор индексирования, примененный к контейнеру map, получает индекс, типом которого должен быть key_type (или тип, допускающий преобразование в него). Возвращает значение типа mapped_type.

Строгое сравнение (strict weak ordering). Отношения между ключами ассоциативного контейнера. При строгом сравнении можно сравнить два любых значения и выяснить, которое из них меньше. Если ни одно из значений не меньше другого, они считаются равными.

Типkey_type. Тип, определенный в шаблоне ассоциативного контейнера, которому соответствует тип ключей, используемых для сохранения и возвращения значения. У контейнера map тип key_type используется для индексации. У контейнера set типы key_type и value_type совпадают.

Типmapped_type. Тип, определенный в шаблонах ассоциативных контейнеров map и multimap, которому соответствует тип хранимых значений.

Типpair (пара). Тип, объект которого содержит две открытые переменные-члена по имени first (первый) и second (второй). Тип pair является шаблоном, при создании класса которого указывают два типа: тип первого и тип второго элемента.

Типvalue_type. Тип элемента, хранимого в контейнере. У контейнеров set и multiset типы value_type и key_type совпадают. У контейнеров map и multimap этот тип представляет собой пару, первый элемент которой (first) имеет тип const key_type, а второй (second) — тип mapped_type.

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

Хеш-функция (hash function). Функция, сопоставляющая значения заданного типа с целочисленными значениями (size_t). Равные значения должны сопоставляться с равными целыми числами; неравные значения должны сопоставляться с неравными целым числами, если это возможно.

 

Глава 12

Динамическая память

 

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

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

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

До сих пор наши программы использовали только статические объекты или объекты, располагаемые в стеке. Статическая память используется для локальных статических переменных (см. раздел 6.1.1), для статических переменных-членов классов (см. раздел 7.6), а также для переменных, определенных вне функций. Стек используется для нестатических объектов, определенных в функциях. Объекты, расположенные в статической памяти или в стеке, автоматически создаются и удаляются компилятором. Объекты из стека существуют, только пока выполняется блок, в котором они определены; статические объекты создаются прежде, чем они будут использованы, и удаляются по завершении программы.

Кроме статической памяти и стека, у каждой программы есть также пул памяти, которую она может использовать. Это динамическая память (free store) или распределяемая память (heap). Программы используют распределяемую память для объектов, называемых динамически созданными объектами (dynamically allocated object), место для которых программа резервирует во время выполнения. Программа сама контролирует продолжительность существования динамических объектов; наш код должен явно освобождать такие объекты, когда они больше не нужны.

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

 

12.1. Динамическая память и интеллектуальные указатели

 

Для управления динамической памятью в языке С++ используются два оператора: оператор new, который резервирует (а при необходимости и инициализирует) объект в динамической памяти и возвращает указатель на него; и оператор delete, который получает указатель на динамический объект и удаляет его, освобождая зарезервированную память.

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

Чтобы сделать использование динамической памяти проще (и безопасный), новая библиотека предоставляет два типа интеллектуальных указателей (smart pointer) для управления динамическими объектами. Интеллектуальный указатель действует, как обычный указатель, но с важным дополнением: автоматически удаляет объект, на который он указывает. Новая библиотека определяет два вида интеллектуальных указателей, отличающихся способом управления своими базовыми указателями: указатель shared_ptr позволяет нескольким указателям указывать на тот же объект, а указатель unique_ptr — нет. Библиотека определяет также сопутствующий класс weak_ptr, являющийся второстепенной ссылкой на объект, управляемый указателем shared_ptr. Все три класса определены в заголовке memory.

 

12.1.1. Класс

shared_ptr

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

shared_ptr p1;    // shared_ptr может указывать на строку

shared_ptr> p2; // shared_ptr может указывать на

                          // список целых чисел

Инициализированный по умолчанию интеллектуальный указатель хранит нулевой указатель (см. раздел 2.3.2). Дополнительные способы инициализации интеллектуального указателя рассматриваются в разделе 12.1.3.

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

// если указатель p1 не нулевой и не указывает на пустую строку

if (p1 && p1->empty())

 *p1 = "hi"; // обратиться к значению p1, чтобы присвоить ему

             // новое значение строки

Список общих функций указателей shared_ptr и unique_ptr приведен в табл. 12.1. Функции, специфические для указателя shared_ptr, перечислены в табл. 12.2.

Таблица 12.1. Функции, общие для указателей shared_ptr и unique_ptr

shared_ptr<T> sp unique_ptr<T> up Нулевой интеллектуальный указатель, способный указывать на объекты типа Т
p При использовании указателя p в условии возвращается значение true , если он указывает на объект
*p Обращение к значению указателя p возвращает объект, на который он указывает
p->mem Синоним для (*p).mem
p.get() Возвращает указатель, хранимый указателем p . Используйте его осторожно, поскольку объект, на который он указывает, может прекратить существование после удаления его интеллектуальным указателем
swap(p, q) p.swap(q) Обменивает указатели в p и q

Таблица 12.2. Функции, специфические для указателя shared_ptr

make_shared<T>( args ) Возвращает указатель shared_ptr на динамически созданный объект типа Т . Аргументы args используются для инициализации создаваемого объекта
shared_ptr<T> p(q) p — копия shared_ptr q ; инкремент счетчика q . Тип содержащегося в q указателя должен быть приводим к типу Т* (см. раздел 4.11.2)
p = q p и q — указатели shared_ptr , содержащие указатели, допускающие приведение друг к другу. Происходит декремент счетчика ссылок p и инкремент счетчика q ; если счетчик указателя p достиг 0, память его объекта освобождается
p.unique() Возвращает true , если p.use_count() равно единице, и значение false в противном случае
p.use_count() Возвращает количество объектов, совместно использующих указатель p ; может выполняться очень медленно, предназначена прежде всего для отладки

Функция make_shared()

Наиболее безопасный способ резервирования и использования динамической памяти подразумевает вызов библиотечной функции make_shared(). Она резервирует и инициализирует объект в динамической памяти, возвращая указатель типа shared_ptr на этот объект. Как и типы интеллектуальных указателей, функция make_shared() определена в заголовке memory.

При вызове функции make_shared() следует указать тип создаваемого объекта. Это подобно использованию шаблона класса — за именем функции следует указание типа в угловых скобках:

// указатель shared_ptr на объект типа int со значением 42

shared_ptr p3 = make_shared(42);

// р4 указывает на строку со значением '9999999999'

shared_ptr р4 = make_shared(10, '9');

// р5 указывает на объект типа int со значением по

// умолчанию (p. 3.3.1) 0

shared_ptr р5 = make_shared();

Подобно функции-члену emplace() последовательного контейнера (см. раздел 9.3.1), функция make_shared() использует свои аргументы для создания объекта заданного типа. Например, при вызове функции make_shared() следует передать аргумент (аргументы), соответствующий одному из конструкторов типа string. Вызову функции make_shared() можно передать любое значение, которое можно использовать для инициализации переменной типа int, и т.д. Если не передать аргументы, то объект инициализируется значением по умолчанию (см. раздел 3.3.1).

Для облегчения определения объекта, содержащего результат вызова функции make_shared(), обычно используют ключевое слово auto (см. раздел 2.5.2):

// p6 указывает на динамически созданный пустой вектор vector

auto p6 = make_shared>();

Копирование и присвоение указателей shared_ptr

При копировании и присвоении указателей shared_ptr каждый из них отслеживает количество других указателей shared_ptr на тот же объект:

auto p = make_shared(42); // объект, на который указывает p

                               // имеет только одного владельца

auto q(p); // p и q указывают на тот же объект

// объект, на который указывают p и q, имеет двух владельцев

С указателем shared_ptr связан счетчик, обычно называемый счетчиком ссылок (reference count). При копировании указателя shared_ptr значение счетчика увеличивается. Например, значение связанного с указателем shared_ptr счетчика увеличивается, когда он используется для инициализации другого указателя shared_ptr, а также при использовании его в качестве правого операнда присвоения, или при передаче его функции (см. раздел 6.2.1), или при возвращении из функции по значению (см. раздел 6.3.2). Значение счетчика увеличивается при присвоении нового значения указателю shared_ptr, а когда он удаляется или когда локальный указатель shared_ptr выходит из области видимости (см. раздел 6.1.1), значение счетчика уменьшается.

Как только счетчик указателя shared_ptr достигает нуля, он автоматически освобождает объект, на который указывает:

auto r = make_shared(42); // объект int, на который указывает r,

                               // имеет одного владельца

r = q; // присвоение r переводит этот указатель на другой адрес

// приращение счетчика владельцев объекта, на который указывает q

// уменьшение счетчика владельцев объекта, на который указывает r

// объект, на который указывал r, не имеет более владельцев;

// он освобождается автоматически

Здесь резервируется переменная типа int, а ее адрес сохраняется в указателе r. Затем указателю r присваивается новое значение. В данном случае r — единственный указатель типа shared_ptr, указывающий на этот объект. В результате присвоения r = q переменная int автоматически освобождается.

Будет ли использован счетчик или другая структура данных для отслеживания количества указателей на совместно используемый объект, зависит от реализации компилятора. Главное то, что класс отслеживает количество указателей shared_ptr на тот же объект и автоматически освобождает его в подходящий момент.

Указатель shared_ptr автоматически удаляет свои объекты…

Когда последний указатель shared_ptr на объект удаляется, его класс автоматически удаляет объект, на который он указывает. Для этого используется другая специальная функция-член — деструктор (destructor), аналогичная конструкторам, которые есть у каждого класса. Подобно тому, как конструктор контролирует инициализацию, деструктор контролирует происходящее при удалении объектов этого типа.

Деструкторы обычно освобождают ресурсы, зарезервированные объектом. Например, конструкторы класса string (как и другие его члены) резервируют память для содержания составляющих ее символов. Деструктор класса string освобождает эту память. Точно так же некоторые функции класса vector резервируют память для хранения элементов вектора. Деструктор класса vector удаляет эти элементы и освобождает используемую ими память.

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

…и автоматически освобождает их память

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

// функция factory() возвращает указатель shared_ptr на динамически

// созданный объект

shared_ptr factory(Т arg) {

 // обработать аргумент соответствующим образом

 // shared_ptr позаботится об освобождении этой памяти

 return make_shared(arg);

}

Функция factory() возвращает указатель shared_ptr, гарантирующий удаление созданного ею объекта в подходящий момент. Например, следующая функция сохраняет указатель shared_ptr, возвращенный функцией factory(), в локальной переменной:

void use_factory(Т arg) {

 shared_ptr p = factory(arg);

 // использует p

} // p выходит из области видимости; память, на которую он указывал,

  // освобождается автоматически

Поскольку указатель p является локальным для функции use_factory(), он удаляется по ее завершении (см. раздел 6.1.1). Когда указатель p удаляется, осуществляется декремент его счетчика ссылок и проверка. В данном случае p — единственный указатель на объект в памяти, возвращенный функцией factory(). Поскольку указатель p выходит из области видимости, объект, на который он указывает, удаляется, а память, в которой он располагался, освобождается.

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

shared_ptr use_factory(Т arg) {

 shared_ptr p = factory(arg);

 // использует p

 return p; // при возвращении p счетчик ссылок увеличивается

} // p выходит из области видимости; память, на которую он указывал,

  // не освобождается

В этой версии функции use_factory() оператор return возвращает вызывающей стороне (см. раздел 6.3.2) копию указателя p. Копирование указателя shared_ptr добавляет единицу к счетчику ссылок этого объекта. Теперь, когда указатель p удаляется, останется другой владелец области памяти, на которую указывал указатель p. Класс shared_ptr гарантирует, что пока есть хоть один указатель shared_ptr на данную область памяти, она не будет освобождена.

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

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

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

Обычно динамическую память используют в следующих случаях.

1. Неизвестно необходимое количество объектов.

2. Неизвестен точный тип необходимых объектов.

3. Нельзя разрешать совместное использование данных несколькими объектами.

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

Использованные до сих пор классы резервировали ресурсы, которые существовали, только пока существовал объект. Например, каждому вектору принадлежат его собственные элементы. При копировании вектора элементы исходного вектора копировались в независимые элементы другого:

vector v1; // пустой вектор

{ // новая область видимости

 vector v2 = {"a", "an", "the"};

 v1 = v2; // копирует элементы из v2 в v1

} // v2 удаляется, что удаляет элементы v2

  // v1 содержит три элемента, являющихся копиями элементов v2

Элементы вектора существуют, только пока существует сам вектор. Когда вектор удаляется, удаляются и его элементы.

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

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

Blob b1; // пустой Blob

{ // новая область видимости

 Blob b2 = {"a", "an", "the"};

 b1 = b2; // b1 и b2 совместно используют те же элементы

} // b2 удаляется, но элементы b2 нет

  // b1 указывает на элементы, первоначально созданные в b2

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

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

Определение класса StrBlob

В конечном счете класс Blob будет реализован как шаблон, но это только в разделе 16.1.2, а пока определим его версию, способную манипулировать только строками. Поэтому назовем данную версию этого класса StrBlob.

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

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

Для реализации совместного использования снабдим каждый объект класса StrBlob указателем shared_ptr на вектор в динамической памяти. Указатель-член shared_ptr будет следить за количеством объектов класса StrBlob, совместно использующих тот же самый вектор, и удалит его, когда будет удален последний объект класса StrBlob.

Осталось решить, какие функции будет предоставлять создаваемый класс. Реализуем пока небольшое подмножество функций вектора. Изменим также функции обращения к элементам (включая front() и back()): в данном классе при попытке доступа к не существующим элементам они будут передавать исключения.

У класса будет стандартный конструктор и конструктор с параметром типа initializer_list (см. раздел 6.2.6). Этот конструктор будет получать список инициализаторов в скобках.

class StrBlob {

public:

 typedef std::vector::size_type size_type;

 StrBlob();

 StrBlob(std::initializer_list il);

 size_type size() const { return data->size(); }

 bool empty() const { return data->empty(); }

 // добавление и удаление элементов

 void push_back(const std::string &t) {data->push_back(t);}

 void pop_back();

 // доступ к элементам

 std::string& front();

 std::string& back();

private:

 std::shared_ptr> data;

 // передать сообщение при недопустимости data[i]

 void check(size_type i, const std::string &msg) const;

};

В классе будут реализованы функции-члены size(), empty() и push_back(), которые передают свою работу через указатель data внутреннему вектору. Например, функция size() класса StrBlob вызывает функцию data->size() и т.д.

Конструкторы класса StrBlob

Для инициализации своей переменной-члена data указателем на динамически созданный вектор каждый конструктор использует собственный список инициализации (см. раздел 7.1.4). Стандартный конструктор резервирует пустой вектор:

StrBlob::StrBlob(): data(make_shared>()) { }

StrBlob::StrBlob(initializer_list il):

 data(make_shared>(il)) { }

Конструктор, получающий тип initializer_list, передает свой параметр для соответствующего конструктора класса vector (см. раздел 2.2.1). Этот конструктор инициализирует элементы вектора копиями значений из списка.

Функции-члены доступа к элементам

Функции pop_back(), front() и back() обращаются к соответствующим функциям-членам вектора. Эти функции должны проверять существование элементов прежде, чем попытаться получить доступ к ним. Поскольку несколько функций-членов должны осуществлять ту же проверку, снабдим класс закрытой вспомогательной функцией check(), проверяющей принадлежность заданного индекса диапазону. Кроме индекса, функция check() получает аргумент типа string, передаваемый обработчику исключений. Строка описывает то, что пошло не так, как надо:

void StrBlob::check(size_type i, const string &msg) const {

 if (i >= data->size())

  throw out_of_range(msg);

}

Функция pop_back() и функции-члены доступа к элементам сначала вызывают функцию check(). Если проверка успешна, эти функции-члены передают свою работу соответствующим функциям вектора:

strings StrBlob::front() {

 // если вектор пуст, функция check() передаст следующее

 check(0, "front on empty StrBlob");

 return data->front();

}

strings StrBlob::back() {

 check(0, "back on empty StrBlob");

 return data->back();

}

void StrBlob::pop_back() {

 check(0, "pop_back on empty StrBlob");

 data->pop_back();

}

Функции-члены front() и back() должны быть перегружены для констант (см. раздел 7.3.2). Определение этих версий остается в качестве самостоятельного упражнения.

Копирование, присвоение и удаление объектов класса StrBlob

Подобно классу Sales_data, класс StrBlob использует стандартные версии функций копирования, присвоения и удаления объектов (см. раздел 7.1.5). По умолчанию эти функции копируют, присваивают и удаляют переменные-члены класса. У класса StrBlob есть только одна переменная-член — указатель shared_ptr. Поэтому при копировании, присвоении и удалении объекта класса StrBlob его переменная-член shared_ptr будет скопирована, присвоена или удалена.

Как уже упоминалось выше, копирование указателя shared_ptr приводит к инкременту его счетчика ссылок; присвоение одного указателя shared_ptr другому приводит к инкременту счетчика правого операнда и декременту счетчика левого; удаление указателя shared_ptr приводит к декременту его счетчика. Если значение счетчика указателя shared_ptr доходит до нуля, объект, на который он указывает, удаляется автоматически. Таким образом, вектор, созданный конструкторами класса StrBlob, будет автоматически удален при удалении последнего объекта класса StrBlob, указывающего на этот вектор.

Упражнения раздела 12.1.1

Упражнение 12.1. Сколько элементов будут иметь объекты b1 и b2 в конце этого кода?

StrBlob b1; {

 StrBlob b2 = {"a", "an", "the"};

 b1 = b2;

 b2.push_back("about");

}

Упражнение 12.2. Напишите собственную версию класса StrBlob, включающего константные версии функций front() и back().

Упражнение 12.3. Нуждается ли этот класс в константных версиях функций push_back() и pop_back()? Если они нужны, добавьте их. В противном случае объясните, почему они не нужны?

Упражнение 12.4. В функции check() нет проверки того, что параметр i больше нуля. Почему эта проверка не нужна?

Упражнение 12.5. Конструктор, получающий тип initializer_list, не был объявлен как explicit (см. раздел 7.5.4). Обсудите преимущества и недостатки этого выбора.

 

12.1.2. Непосредственное управление памятью

Язык определяет два оператора, позволяющие резервировать и освобождать области в динамической памяти. Оператор new резервирует память, а оператор delete освобождает память, зарезервированную оператором new.

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

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

Использование оператора new для динамического резервирования и инициализации объектов

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

int *pi = new int; // pi указывает на динамически созданный,

                   //  безымянный,

                   // неинициализированный объект типа int

Это выражение new создает в динамической памяти объект типа int и возвращает указатель на него.

По умолчанию создаваемые в динамической памяти объекты инициализируются значением по умолчанию (см. раздел 2.2.1). Это значит, что у объектов встроенного или составного типа будет неопределенное значение, а объекты типа класса инициализируются их стандартным конструктором:

string *ps = new string; // инициализируется пустой строкой

int *pi = new int;       // pi указывает на неинициализированный int

Динамически созданный объект можно инициализировать, используя прямую инициализацию (см. раздел 3.2.1). Можно применить традиционный конструктор (используя круглые скобки), а по новому стандарту можно также использовать списочную инициализацию (с фигурными скобками):

int *pi = new int(1024); // pi указывает на объект со значением 1024

string *ps = new string(10, '9'); // *ps = "9999999999"

// вектор на десять элементов со значениями от 0 до 9

vector *pv = new vector{0,1,2,3,4,5,6,7,8,9};

Динамически созданный объект можно также инициализировать значением по умолчанию (см. раздел 3.3.1), сопроводив имя типа парой пустых круглых скобок:

string *ps1 = new string;  // инициализация по умолчанию пустой строкой

string *ps = new string(); // инициализация значением по умолчанию

                           // (пустой строкой)

int *pi1 = new int;        // инициализация по умолчанию;

                           // значение *pi1 не определено

int *pi2 = new int();      // инициализация значением по умолчанию 0;

                           // *pi2 = 0

Для типов классов (таких как string), определяющих собственные конструкторы (см. раздел 7.1.4), запрос инициализации значением по умолчанию не имеет последствий; независимо от формы, объект инициализируется стандартным конструктором. Различие существенно в случае встроенных типов: инициализация объекта встроенного типа значением по умолчанию присваивает ему вполне конкретное значение, а инициализация по умолчанию — нет. Точно так же полагающиеся на синтезируемый стандартный конструктор члены класса встроенного типа также не будут не инициализированы, если эти члены не будут инициализированы в теле класса (см. раздел 7.1.4).

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

Когда предоставляется инициализатор в круглых скобках, для вывода типа объекта, который предстоит зарезервировать для этого инициализатора, можно использовать ключевое слово auto (см. раздел 2.5.2). Но, поскольку компилятор использует тип инициализатора для вывода резервируемого типа, ключевое слово auto можно использовать только с одиночным инициализатором в круглых скобках:

auto p1 = new auto(obj); // p указывает на объект типа obj

                         // этот объект инициализируется значением obj

auto p2 = new auto{a,b,c}; // ошибка: для инициализатора нужно

                           // использовать круглые скобки

Тип p1 — это указатель на автоматически выведенный тип obj. Если obj имеет тип int, то тип p1 — int*; если obj имеет тип string, то тип p1 — string* и т.д. Вновь созданный объект инициализируется значением объекта obj.

Динамически созданные константные объекты

Для резервирования константных объектов вполне допустимо использовать оператор new:

// зарезервировать и инициализировать

const int const int *pci = new const int(1024);

// зарезервировать и инициализировать значением по умолчанию

const string const string *pcs = new const string;

Подобно любым другим константным объектам, динамически созданный константный объект следует инициализировать. Динамический константный объект типа класса, определяющего стандартный конструктор (см. раздел 7.1.4), можно инициализировать неявно. Объекты других типов следует инициализировать явно. Поскольку динамически зарезервированный объект является константой, возвращенный оператором new указатель является указателем на константу (см. раздел 2.4.2).

Исчерпание памяти

Хотя современные машины имеют огромный объем памяти, всегда существует вероятность исчерпания динамической памяти. Как только программа использует всю доступную ей память, выражения с оператором new будут терпеть неудачу. По умолчанию, если оператор new неспособен зарезервировать требуемый объем памяти, он передает исключение типа bad_alloc (см. раздел 5.6). Используя иную форму оператора new, можно воспрепятствовать передаче исключения:

// при неудаче оператор new возвращает нулевой указатель

int *p1 = new int; // при неудаче оператор new передает

                   // исключение std::bad_alloc

int *p2 = new (nothrow) int; // при неудаче оператор new возвращает

                             // нулевой указатель

По причинам, рассматриваемым в разделе 19.1.2, эта форма оператора new упоминается как размещающий оператор new (placement new). Выражение размещающего оператора new позволяет передать дополнительные аргументы. В данном случае передается определенный библиотекой объект nothrow. Передача объекта nothrow оператору new указывает, что он не должен передавать исключения. Если эта форма оператора new окажется неспособна зарезервировать требуемый объем памяти, она возвратит нулевой указатель. Типы bad_alloc и nothrow определены в заголовке new.

Освобождение динамической памяти

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

delete p; // p должен быть указателем на динамически созданный объект

          // или нулевым указателем

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

Значения указателя и оператор delete

Передаваемый оператору delete указатель должен либо указывать на динамически созданный объект, либо быть нулевым указателем (см. раздел 2.3.2). Результат удаления указателя на область памяти, зарезервированную не оператором new, или повторного удаления значения того же указателя непредсказуем:

int i, *pi1 = &i, *pi2 = nullptr;

double *pd = new double(33), *pd2 = pd;

delete i;   // ошибка: i - не указатель

delete pi1; // непредсказуемо: pi1 - локальный

delete pd;  // ok

delete pd2; // непредсказуемо: память, на которую указывает pd2,

            // уже освобождена

delete pi2; // ok: освобождение нулевого указателя всегда допустимо

Компилятор сообщает об ошибке оператора delete i, поскольку знает, что i — не указатель. Ошибки, связанные с выполнением оператора delete для указателей pi1 и pd2, коварней: обычно компиляторы неспособны выяснить, указывает ли указатель на объект, созданный статически или динамически. Точно так же компилятор не может установить, была ли уже освобождена память, на которую указывает указатель. Большинство компиляторов примет такие выражения delete, несмотря на их ошибочность.

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

const int *pci = new const int(1024);

delete pci; // ok: удаляет константный объект

Динамически созданные объекты существуют до тех пор, пока не будут освобождены

Как упоминалось в разделе 12.1.1, управляемая указателем shared_ptr память автоматически освобождается при удалении последнего указателя shared_ptr. Динамический объект, управляемый указателем встроенного типа, существует до тех пор, пока к областям памяти, управляемой при помощи указателей встроенных типов, не будет удален явно.

Функции, возвращающие обычные (а не интеллектуальные) указатели на области динамической памяти, возлагают ответственность за их удаление на вызывающую сторону:

// возвращает указатель на динамически созданный объект

Foo* factory(Т arg) {

 // обработать аргумент соответственно

 return new Foo(arg); // за освобождение этой памяти отвечает

                      // вызывающая сторона

}

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

void use_factory(Т arg) {

 Foo *p = factory(arg);

 // использовать p, но не удалить его

} // p выходит из области видимости, но память,

  //  на которую он указывает,  не освобождается!

Здесь функция use_factory() вызывает функцию factory() резервирующую новый объект типа Foo. Когда функция use_factory() завершает работу, локальная переменная p удаляется. Эта переменная — встроенный указатель, а не интеллектуальный.

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

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

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

void use_factory(Т arg) {

 Foo *p = factory(arg);

 // использование p

 delete p; // не забыть освободить память сейчас, когда

           // она больше не нужна

}

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

Foo* use_factory(Т arg) {

 Foo *p = factory(arg);

 // использование p

 return p; // освободить память должна вызывающая сторона

}

Внимание! Управление динамической памятью подвержено ошибкам

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

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

2. Объект использован после удаления. Иногда эта ошибка обнаруживается при создании нулевого указателя после удаления.

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

Допустить эти ошибки значительно проще, чем потом найти и исправить.

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

Переустановка значения указателя после удаления…

Когда указатель удаляется, он становится недопустимым. Но, даже став недопустимым, на многих машинах он продолжает содержать адрес уже освобожденной области динамической памяти. После освобождения области памяти указатель на нее становится потерянным указателем (dangling pointer). Потерянный указатель указывает на ту область памяти, которая когда-то содержала объект, но больше не содержит.

Потерянным указателям присущи все проблемы неинициализированных указателей (см. раздел 2.3.2). Проблем с потерянными указателями можно избежать, освободив связанную с ними память непосредственно перед выходом из области видимости самого указателя. Так не появится шанса использовать указатель уже после того, как связанная с ним память будет освобождена. Если указатель необходимо сохранить, то после применения оператора delete ему можно присвоить значение nullptr. Это непосредственно свидетельствует о том, что указатель не указывает на объект.

…обеспечивает лишь частичную защиту

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

int *p(new int(42)); // p указывает на динамическую память

auto q = p;          // p и q указывают на ту же область памяти

delete p;            // делает недопустимыми p и q

p = nullptr; // указывает, что указатель p больше не связан с объектом

Здесь указатели p и q указывают на тот же динамически созданный объект. Удалим этот объект и присвоим указателю p значение nullptr, засвидетельствовав, что он больше не указывает на объект. Однако переустановка значения указателя p никак не влияет на указатель q, который стал недопустимым после освобождения памяти, на которую указывал указатель p (и указатель q!). В реальных системах поиск всех указателей на ту же область памяти зачастую на удивление труден.

Упражнения раздела 12.1.2

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

Упражнение 12.7. Переделайте предыдущее упражнение, используя на сей раз указатель shared_ptr.

Упражнение 12.8. Объясните, все ли правильно в следующей функции:

bool b() {

 int* p = new int;

 // ...

 return p;

}

Упражнение 12.9. Объясните, что происходит в следующем коде:

int *q = new int(42), *r = new int(100);

r = q;

auto q2 = make_shared(42), r2 = make_shared(100);

r2 = q2;

 

12.1.3. Использование указателя

shared_ptr

с оператором

new

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

shared_ptr p1; // shared_ptr может указывать на double

shared_ptr p2(new int(42)); // p2 указывает на int со значением 42

Конструкторы интеллектуального указателя, получающие указатели, являются явными (см. раздел 7.5.4). Следовательно, нельзя неявно преобразовать встроенный указатель в интеллектуальный; для инициализации интеллектуального указателя придется использовать прямую форму инициализации (см. раздел 3.2.1):

shared_ptr p1 = new int(1024); // ошибка: нужна

                                    //  прямая инициализация

shared_ptr p2(new int(1024));  // ok: использует

                                    //  прямую инициализацию

Таблица 12.3. Другие способы определения и изменения указателя shared_ptr

shared_ptr<T> p(q) Указатель p управляет объектом, на который указывает указатель встроенного типа q ; указатель q должен указывать на область памяти, зарезервированную оператором new , а его тип должен быть преобразуем в тип Т*
shared_ptr<T> p(u) Указатель p учитывает собственность указателя u типа unique_ptr ; указатель u становится нулевым
shared_ptr<T> p(q, d) Указатель p учитывает собственность объекта, на который указывает встроенный указатель q . Тип указателя q должен быть преобразуем в тип Т* (см. раздел 4.11.2). Для освобождения q указатель p будет использовать вызываемый объект d (см. раздел 10.3.2) вместо оператора delete
shared_ptr<T> p(p2, d) Указатель p — это копия указателя p2 типа shared_ptr , как описано в табл. 12.2, за исключением того, что указатель p использует вызываемый объект d вместо оператора delete
p.reset() p.reset(q) p.reset(q, d) Если p единственный указатель shared_ptr на объект, функция reset() освободит существующий объект p . Если передан необязательный встроенный указатель q , то p будет указывать на q , в противном случае p станет нулевым. Если предоставлен вызываемый объект d , то он будет вызван для освобождения указателя q , в противном случае используется оператор delete

Инициализация указателя p1 неявно требует, чтобы компилятор создал указатель типа shared_ptr из указателя int*, возвращенного оператором new. Поскольку нельзя неявно преобразовать обычный указатель в интеллектуальный, такая инициализация ошибочна. По той же причине функция, возвращающая указатель shared_ptr, не может неявно преобразовать простой указатель в своем операторе return:

shared_ptr clone(int p) {

 return new int(p); // ошибка: неявное преобразование

                    // в shared_ptr

}

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

shared_ptr clone (int p) {

 // ok: явное создание shared_ptr из int*

 return shared_ptr(new int(p));

}

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

#magnify.png Не смешивайте обычные указатели с интеллектуальными

Указатель shared_ptr может координировать удаление только с другими указателями shared_ptr, которые являются его копиями. Действительно, этот факт — одна из причин, по которой рекомендуется использовать функцию make_shared(), а не оператор new. Это связывает указатель shared_ptr с объектом одновременно с его резервированием. При этом нет никакого способа по неосторожности связать ту же область памяти с несколькими независимо созданными указателями shared_ptr.

Рассмотрим следующую функцию, работающую с указателем shared_ptr:

// ptr создается и инициализируется при вызове process()

void process(shared_ptr ptr) {

 // использование ptr

} // ptr выходит из области видимости и удаляется

Параметр функции process() передается по значению, поэтому аргумент копируется в параметр ptr. Копирование указателя shared_ptr осуществляет инкремент его счетчика ссылок. Таким образом, в функции process() значение счетчика не меньше 2. По завершении функции process() осуществляется декремент счетчика ссылок указателя ptr, но он не может достигнуть нуля. Поэтому, когда локальная переменная ptr удаляется, память, на которую она указывает, не освобождается.

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

shared_ptr p(new int (42)); // счетчик ссылок = 1

process(p); // копирование p увеличивает счетчик;

            // в функции process() счетчик = 2

int i = *p; // ok: счетчик ссылок = 1

Хотя функции process() нельзя передать встроенный указатель, ей можно передать временный указатель shared_ptr, явно созданный из встроенного указателя. Но это, вероятно, будет ошибкой:

int *x(new int(1024)); // опасно: x - обычный указатель, a

                       // не интеллектуальный process(x);

// ошибка: нельзя преобразовать int* в shared_ptr

process(shared_ptr(x)); // допустимо, но память будет освобождена!

int j = *x; // непредсказуемо: x - потерянный указатель!

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

Но указатель x продолжает указывать на эту (освобожденную) область памяти; теперь x — потерянный указатель. Результат попытки использования значения, на которое указывает указатель x, непредсказуем.

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

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

Другие операции с указателем shared_ptr

Класс shared_ptr предоставляет также несколько других операций, перечисленных в табл. 12.2 и табл. 12.3. Чтобы присвоить новый указатель указателю shared_ptr, можно использовать функцию reset():

p = new int(1024); // нельзя присвоить обычный указатель

                   // указателю shared_ptr

p.reset(new int(1024)); // ok: p указывает на новый объект

Подобно оператору присвоения, функция reset() модифицирует счетчики ссылок, а если нужно, удаляет объект, на который указывает указатель p. Функцию-член reset() зачастую используют вместе с функцией unique() для контроля совместного использования объекта несколькими указателями shared_ptr. Прежде чем изменять базовый объект, проверяем, является ли владелец единственным. В противном случае перед изменением создается новая копия:

if (!p.unique())

 p.reset(new string(*p)); // владелец не один; резервируем новую копию

*p += newVal; // теперь, когда известно, что указатель единственный,

              // можно изменить объект

Упражнения раздела 12.1.3

Упражнение 12.10. Укажите, правилен ли следующий вызов функции process(), определенной в текущем разделе. В противном случае укажите, как его исправить?

shared_ptr p(new int(42));

process(shared_ptr(p));

Упражнение 12.11. Что будет, если вызвать функцию process() следующим образом?

process(shared_ptr(p.get()));

Упражнение 12.12. Используя объявления указателей p и sp, объясните каждый из следующих вызовов функции process(). Если вызов корректен, объясните, что он делает. Если вызов некорректен, объясните почему:

auto p = new int();

auto sp = make_shared();

(a) process(sp);

(b) process(new int());

(c) process(p);

(d) process(shared_ptr(p));

Упражнение 12.13. Что будет при выполнении следующего кода?

auto sp = make_shared();

auto p = sp.get();

delete p;

 

12.1.4. Интеллектуальные указатели и исключения

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

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

void f() {

 shared_ptr sp(new int(42)); // зарезервировать новый объект

 // код, передающий исключение, не обрабатываемое в функции f()

} // shared_ptr освобождает память автоматически по завершении функции

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

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

void f() {

 int *ip = new int(42); // динамически зарезервировать новый объект

 // код, передающий исключение, не обрабатываемое в функции f()

 delete ip; // освобождает память перед выходом

}

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

#books.png Интеллектуальные указатели и классы без деструкторов

Большинство классов языка С++, включая все библиотечные классы, определяют деструкторы (см. раздел 12.1.1), заботящиеся об удалении используемых объектом ресурсов. Но не все классы таковы. В частности, классы, разработанные для использования и в языке С, и в языке С++, обычно требуют от пользователя явного освобождения всех используемых ресурсов.

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

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

struct destination; // представляет то, с чем установлено соединение

struct connection;  // информация для использования соединения

connection connect(destination*); // открывает соединение

void disconnect(connection);      // закрывает данное соединение

void f(destination &d /* другие параметры */) {

 // получить соединение; не забыть закрывать по завершении

 connection с = connect(&d); // использовать соединение

 // если забыть вызывать функцию disconnect() перед выходом из

 // функции f(), то уже не будет никакого способа закрыть соединение

}

Если бы у структуры connection был деструктор, то по завершении функции f() он закрыл бы соединение автоматически. Однако у нее нет деструктора. Эта проблема почти идентична проблеме предыдущей программы, использовавшей указатель shared_ptr, чтобы избежать утечек памяти. Здесь также можно использовать указатель shared_ptr для гарантии правильности закрытия соединения.

#books.png Использование собственного кода удаления

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

void end_connection(connection *p) { disconnect(*p); }

При создании указателя shared_ptr можно передать необязательный аргумент, указывающий на функцию удаления (см. раздел 6.7):

void f(destination &d /* другие параметры */) {

 connection с = connect(&d);

 shared_ptr p(&c, end_connection);

 // использовать соединение

 // при выходе из функции f(), даже в случае исключения, соединение

 // будет закрыто правильно

}

При удалении указателя p для хранимого в нем указателя вместо оператора delete будет вызвана функция end_connection(). Функция end_connection(), в свою очередь, вызовет функцию disconnect(), гарантируя таким образом закрытие соединения. При нормальном выходе из функции f() указатель p будет удален в ходе процедуры выхода. Кроме того, указатель p будет также удален, а соединение закрыто, если произойдет исключение.

Внимание! Проблемы интеллектуального указателя

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

• Не используйте значение того же встроенного указателя для инициализации (переустановки) нескольких интеллектуальных указателей.

• Не используйте оператор delete для указателя, возвращенного функцией get() .

• Не используйте функцию get() для инициализации или переустановки другого интеллектуального указателя.

• Используя указатель, возвращенный функцией get() , помните, что указатель станет недопустимым после удаления последнего соответствующего интеллектуального указателя.

• Если интеллектуальный указатель используется для управления ресурсом, отличным от области динамической памяти, зарезервированной оператором new , не забывайте использовать функцию удаления (раздел 12.1.4 и раздел 12.1.5).

Упражнения раздела 12.1.4

Упражнение 12.14. Напишите собственную версию функции, использующую указатель shared_ptr для управления соединением.

Упражнение 12.15. Перепишите первое упражнение так, чтобы использовать лямбда-выражение (см. раздел 10.3.2) вместо функции end_connection().

 

12.1.5. Класс

unique_ptr

Указатель unique_ptr "владеет" объектом, на который он указывает. В отличие от указателя shared_ptr, только один указатель unique_ptr может одновременно указывать на данный объект. Объект, на который указывает указатель unique_ptr, удаляется при удалении указателя. Список функций, специфических для указателя unique_ptr, приведен в табл. 12.4. Функции, общие для обоих указателей, приведены в табл. 12.1.

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

unique_ptr p1; // указатель unique_ptr на тип double

unique_ptr p2(new int(42)); // p2 указывает на int со значением 42

Таблица 12.4. Функции указателя unique_ptr (см. также табл. 12.1)

unique_ptr<T> u1 unique_ptr<T, D> u2 Обнуляет указатель unique_ptr , способный указывать на объект типа Т . Указатель u1 использует для освобождения своего указателя оператор delete ; а указатель u2 — вызываемый объект типа D
unique_ptr<T, D> u(d) Обнуляет указатель unique_ptr , указывающий на объекты типа Т . Использует вызываемый объект d типа D вместо оператора delete
u = nullptr Удаляет объект, на который указывает указатель u ; обнуляет указатель u
u.release() Прекращает контроль содержимого указателя u ; возвращает содержимое указателя u и обнуляет его
u.reset() u.reset(q) u.reset(nullptr) Удаляет объект, на который указывает указатель u . Если предоставляется встроенный указатель q , то u будет указывать на его объект. В противном случае указатель u обнуляется

Поскольку указатель unique_ptr владеет объектом, на который указывает, он не поддерживает обычного копирования и присвоения:

unique_ptr p1(new string("Stegosaurus"));

unique_ptr p2(p1); // ошибка: невозможно копирование unique_ptr

unique_ptr p3;

p3 = p2; // ошибка: невозможно присвоение unique_ptr

Хотя указатель unique_ptr нельзя ни присвоить, ни скопировать, можно передать собственность от одного (неконстантного) указателя unique_ptr другому, вызвав функцию release() или reset():

// передает собственность от p1 (указывающего на

// строку "Stegosaurus") к p2

unique_ptr p2(p1.release()); // release() обнуляет p1

unique_ptr p3(new string("Trex"));

// передает собственность от p3 к p2

р2.reset(p3.release()); // reset() освобождает память, на которую

                        // указывал указатель p2

Функция-член release() возвращает указатель, хранимый в настоящее время в указателе unique_ptr, и обнуляет указатель unique_ptr. Таким образом, указатель p2 инициализируется указателем, хранимым в указателе p1, а сам указатель p1 становится нулевым.

Функция-член reset() получает необязательный указатель и переустанавливает указатель unique_ptr на заданный указатель. Если указатель unique_ptr не нулевой, то объект, на который он указывает, удаляется. Поэтому вызов функции reset() указателя p2 освобождает память, используемую строкой со значением "Stegosaurus", передает содержимое указателя p3 указателю p2 и обнуляет указатель p3.

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

p2.release(); // ОШИБКА: p2 не освободит память, и указатель

              // будет потерян

auto p = p2.release(); // ok, но следует не забыть delete(p)

Передача и возвращение указателя unique_ptr

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

unique_ptr clone(int p) {

 // ok: явное создание unique_ptr для int*

 return unique_ptr(new int(p));

}

В качестве альтернативы можно также возвратить копию локального объекта:

unique_ptr clone(int p) {

 unique_ptr ret(new int(p));

 // ...

 return ret;

}

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

Совместимость с прежней версией: класс auto_ptr

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

Хотя указатель auto_ptr все еще присутствует в стандартной библиотеке, вместо него следует использовать указатель unique_ptr .

Передача функции удаления указателю unique_ptr

Подобно указателю shared_ptr, для освобождения объекта, на который указывает указатель unique_ptr, по умолчанию используется оператор delete. Подобно указателю shared_ptr, функцию удаления указателя unique_ptr (см. раздел 12.1.4) можно переопределить. Но по причинам, описанным в разделе 16.1.6, способ применения функции удаления указателем unique_ptr отличается от такового у shared_ptr.

Переопределение функции удаления указателя unique_ptr влияет на тип и способ создания (или переустановки) объектов этого типа. Подобно переопределению оператора сравнения ассоциативного контейнера (см. раздел 11.2.2), тип функции удаления можно предоставить в угловых скобках наряду с типом, на который может указывать указатель unique_ptr. При создании или переустановке объекта этого типа предоставляется вызываемый объект определенного типа:

// p указывает на объект типа objT и использует объект типа delT

// для его освобождения

// он вызовет объект по имени fcn типа delT

unique_ptr p(new objT, fcn);

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

void f(destination &d /* другие необходимые параметры */) {

 connection c = connect(&d); // открыть соединение

 // когда p будет удален, соединение будет закрыто

 unique_ptr

  p(&с, end_connection);

 // использовать соединение

 // по завершении f(), даже при исключении, соединение будет

 // закрыто правильно

}

Для определения типа указателя на функцию используется ключевое слово decltype (см. раздел 2.5.3). Поскольку выражение decltype(end_connection) возвращает тип функции, следует добавить символ *, указывающий, что используется указатель на этот тип (см. раздел 6.7).

Упражнения раздела 12.1.5

Упражнение 12.16. Компиляторы не всегда предоставляют понятные сообщения об ошибках, если осуществляется попытка скопировать или присвоить указатель unique_ptr. Напишите программу, которая содержит эти ошибки, и посмотрите, как компилятор диагностирует их.

Упражнение 12.17. Какие из следующих объявлений указателей unique_ptr недопустимы или вероятнее всего приведут к ошибке впоследствии? Объясните проблему каждого из них.

int ix = 1024, *pi = &ix, *pi2 = new int(2048);

typedef unique_ptr IntP;

(a) IntP p0(ix);            (b) IntP p1(pi);

(c) IntP p2(pi2);           (d) IntP p3(&ix);

(e) IntP p4(new int(2048)); (f) IntP p5(p2.get());

Упражнение 12.18. Почему класс указателя shared_ptr не имеет функции-члена release()?

 

12.1.6. Класс

weak_ptr

Класс weak_ptr (табл. 12.5) представляет интеллектуальный указатель, который не контролирует продолжительность существования объекта, на который он указывает. Он только указывает на объект, который контролирует указатель shared_ptr. Привязка указателя weak_ptr к указателю shared_ptr не изменяет счетчик ссылок этого указателя shared_ptr. Как только последний указатель shared_ptr на этот объект будет удален, удаляется и сам объект. Этот объект будет удален, даже если останется указатель weak_ptr на него. Имя weak_ptr отражает концепцию "слабого" совместного использования объекта.

Создаваемый указатель weak_ptr инициализируется из указателя shared_ptr:

auto p = make_shared(42);

weak_ptr wp(p); // wp слабо связан с p; счетчик ссылок p неизменен

Здесь указатели wp и p указывают на тот же объект. Поскольку совместное использование слабо, создание указателя wp не изменяет счетчик ссылок указателя p; это делает возможным удаление объекта, на который указывает указатель wp.

Таблица 12.5. Функции указателя weak_ptr

weak_ptr<T> w Обнуляет указатель weak_ptr , способный указывать на объект типа T
weak_ptr<T> w(sp) Указатель weak_ptr на тот же объект, что и указатель sp типа shared_ptr . Тип Т должен быть приводим к типу, на который указывает sp
w = p Указатель p может иметь тип shared_ptr или weak_ptr . После присвоения w разделяет собственность с указателем p
w.reset() Обнуляет указатель w
w.use_count() Возвращает количество указателей shared_ptr , разделяющих собственность с указателем w
w.expired() Возвращает значение true , когда функция w.use_count()  должна возвратить нуль, и значение false в противном случае
w.lock() Возвращает нулевой указатель shared_ptr , если функция expired() должна возвратить значение true ; в противном случае возвращает указатель shared_ptr на объект, на который указывает указатель w

Поскольку объект может больше не существовать, нельзя использовать указатель weak_ptr для непосредственного доступа к его объекту. Для этого следует вызвать функцию lock(). Она проверяет существование объекта, на который указывает указатель weak_ptr. Если это так, то функция lock() возвращает указатель shared_ptr на совместно используемый объект. Такой указатель гарантирует существование объекта, на который он указывает, по крайней мере, пока существует этот указатель shared_ptr. Рассмотрим пример:

if (shared_ptr np = wp.lock()) { // true, если np не нулевой

 // в if, np совместно использует свой объект с p

}

Внутренняя часть оператора if доступна только в случае истинности вызова функции lock(). В операторе if использование указателя np для доступа к объекту вполне безопасно.

Проверяемый класс указателя

Для того чтобы проиллюстрировать, насколько полезен указатель weak_ptr, определим вспомогательный класс указателя для нашего класса StrBlob. Класс указателя, назовем его StrBlobPtr, будет хранить указатель weak_ptr на переменную-член data класса StrBlob, которым он был инициализирован. Использование указателя weak_ptr не влияет на продолжительность существования вектора, на который указывает данный объект класса StrBlob. Но можно воспрепятствовать попытке доступа к вектору, которого больше не существует.

Класс StrBlobPtr будет иметь две переменные-члена: указатель wptr, который может быть либо нулевым, либо указателем на вектор в объекте класса StrBlob; и переменную curr, хранящую индекс элемента, который в настоящее время обозначает этот объект. Подобно вспомогательному классу класса StrBlob, у класса указателя есть функция-член check(), проверяющая безопасность обращения к значению StrBlobPtr:

// StrBlobPtr передает исключение при попытке доступа к

// несуществующему элементу

class StrBlobPtr {

 public:

 StrBlobPtr() : curr(0) { }

 StrBlobPtr(StrBlob &a, size_t sz = 0):

  wptr(a.data), curr(sz) { }

 std::string& deref() const;

 StrBlobPtr& incr(); // префиксная версия

private:

 // check() возвращает shared_ptr на вектор, если проверка успешна

 std::shared_ptr>

  check(std::size_t, const std::string&) const;

 // хранит weak_ptr, означая возможность удаления основного вектора

 std::weak_ptr> wptr;

 std::size_t curr; // текущая позиция в пределах массива

};

Стандартный конструктор создает нулевой указатель StrBlobPtr. Список инициализации его конструктора (см. раздел 7.1.4) явно инициализирует переменную-член curr нулем и неявно инициализирует указатель-член wptr как нулевой указатель weak_ptr. Второй конструктор получает ссылку на StrBlob и (необязательно) значение индекса. Этот конструктор инициализирует wptr как указатель на вектор данного объекта класса StrBlob и инициализирует переменную curr значением sz. Используем аргумент по умолчанию (см. раздел 6.5.1) для инициализации переменной curr, чтобы обозначить первый элемент. Как будет продемонстрировано, ниже параметр sz будет использован функцией-членом end() класса StrBlob.

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

Функция-член check() класса StrBlobPtr отличается от таковой у класса StrBlob, поскольку она должна проверять, существует ли еще вектор, на который он указывает:

std::shared_ptr>

StrBlobPtr::check(std::size_t i, const std::string &msg) const {

 auto ret = wptr.lock(); // существует ли еще вектор?

 if (!ret)

  throw std::runtime_error("unbound StrBlobPtr");

 if (i >= ret->size())

  throw std::out_of_range(msg);

 return ret; // в противном случае, возвратить shared_ptr на вектор

}

Так как указатель weak_ptr не влияет на счетчик ссылок соответствующего указателя shared_ptr, вектор, на который указывает StrBlobPtr, может быть удален. Если вектора нет, функция lock() возвратит нулевой указатель. В таком случае любое обращение к вектору потерпит неудачу и приведет к передаче исключения. В противном случае функция check() проверит переданный индекс. Если значение допустимо, функция check() возвратит указатель shared_ptr, полученный из функции lock().

Операции с указателями

Определение собственных операторов рассматривается в главе 14, а пока определим функции deref() и incr() для обращения к значению и инкремента указателя класса StrBlobPtr соответственно.

Функция-член deref() вызывает функцию check() для проверки безопасности использования вектора и принадлежности индекса curr его диапазону:

std::string& StrBlobPtr::deref() const {

 auto p = check(curr, "dereference past end");

 return (*p)[curr]; // (*p) - вектор, на который указывает этот объект

}

Если проверка прошла успешно, то p будет указателем типа shared_ptr на вектор, на который указывает данный указатель StrBlobPtr. Выражение (*p)[curr] обращается к значению данного указателя shared_ptr, чтобы получить вектор, и использует оператор индексирования для доступа и возвращения элемента по индексу curr.

Функция-член incr() также вызывает функцию check():

// префикс: возвратить ссылку на объект после инкремента

StrBlobPtr& StrBlobPtr::incr() {

 // если curr уже указывает на элемент после конца контейнера,

 // его инкремент не нужен

 check(curr, "increment past end of StrBlobPtr");

 ++curr; // инкремент текущего состояния

 return *this;

}

Безусловно, чтобы получить доступ к переменной-члену data, наш класс указателя должен быть дружественным классу StrBlob (см. раздел 7.3.4). Снабдим также класс StrBlob функциями begin() и end(), возвращающими указатель StrBlobPtr на себя:

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

// классу StrBlob

class StrBlobPtr;

class StrBlob {

 friend class StrBlobPtr;

 // другие члены, как в разделе 12.1.1

 // возвратить указатель StrBlobPtr на первый и следующий

 // после последнего элементы

 StrBlobPtr begin() { return StrBlobPtr(*this); }

 StrBlobPtr end()

  { auto ret = StrBlobPtr(*this, data->size());

    return ret; }

};

Упражнения раздела 12.1.6

Упражнение 12.19. Определите собственную версию класса StrBlobPtr и модифицируйте класс StrBlob соответствующим объявлением дружественным, а также функциями-членами begin() и end().

Упражнение 12.20. Напишите программу, которая построчно читает исходный файл в операционной системе класса StrBlob и использует указатель StrBlobPtr для вывода каждого его элемента.

Упражнение 12.21. Функцию-член deref() класса StrBlobPtr можно написать следующим образом:

std::string& deref() const

{ return (*check(curr, "dereference past end"))[curr]; }

Какая версия по-вашему лучше и почему?

Упражнение 12.22. Какие изменения следует внести в класс StrBlobPtr, чтобы получить класс, применимый с типом const StrBlob? Определите класс по имени ConstStrBlobPtr, способный указывать на const StrBlob.

 

12.2. Динамические массивы

 

Операторы new и delete резервируют объекты по одному. Некоторым приложениям нужен способ резервировать хранилище для многих объектов сразу. Например, векторы и строки хранят свои элементы в непрерывной памяти и должны резервировать несколько элементов сразу всякий раз, когда контейнеру нужно повторное резервирование (см. раздел 9.4).

Для этого язык и библиотека предоставляют два способа резервирования всего массива объектов. Язык определяет второй вид оператора new, резервирующего и инициализирующего массив объектов. Библиотека предоставляет шаблон класса allocator, позволяющий отделять резервирование от инициализации. По причинам, описанным в разделе 12.2.2, применение класса allocator обычно обеспечивает лучшую производительность и более гибкое управление памятью.

У многих (возможно, у большинства) приложений нет никакой непосредственной необходимости в динамических массивах. Когда приложение нуждается в переменном количестве объектов, практически всегда проще, быстрей и безопасней использовать вектор (или другой библиотечный контейнер), как было сделано в классе StrBlob. По причинам, описанным в разделе 13.6, преимущества использования библиотечного контейнера даже более явны по новому стандарту. Библиотеки, поддерживающие новый стандарт, работают существенно быстрее, чем предыдущие версии.

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

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

Не резервируйте динамические массивы в классах, пока не прочитаете главу 13.

 

12.2.1. Оператор

new

и массивы

Чтобы запросить оператор new зарезервировать массив объектов, после имени типа следует указать в квадратных скобках количество резервируемых объектов. В данном случае оператор new резервирует требуемое количество объектов и (при успешном резервировании) возвращает указатель на первый из них:

// вызов get_size() определит количество резервируемых целых чисел

int *pia = new int[get_size()]; // pia указывает на первое из них

Значение в скобках должно иметь целочисленный тип, но не обязано быть константой.

Для представления типа массива при резервировании можно также использовать псевдоним типа (см. раздел 2.5.1). В данном случае скобки не нужны:

typedef int arrT[42]; // arrT - имя типа массива из 42 целых чисел

int *p = new arrT;    // резервирует массив из 42 целых чисел;

                      // p указывает на первый его элемент

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

int *p = new int[42];

Резервирование массива возвращает указатель на тип элемента

Хотя обычно память, зарезервированную оператором new T[], называют "динамическим массивом", это несколько вводит в заблуждение. Когда мы используем оператор new для резервирования массива, объект типа массива получен не будет. Вместо этого будет получен указатель на тип элемента массива. Даже если для определения типа массива использовать псевдоним типа, оператор new не резервирует объект типа массива. И в данном случае резервируется массив, хотя часть [ число ] не видима. Даже в этом случае оператор new возвращает указатель на тип элемента.

Поскольку зарезервированная память не имеет типа массива, для динамического массива нельзя вызвать функцию begin() или end() (см. раздел 3.5.3). Для возвращения указателей на первый и следующий после последнего элементы эти функции используют размерность массива (являющуюся частью типа массива). По тем же причинам для обработки элементов так называемого динамического массива нельзя также использовать серийный оператор for. 

Важно помнить, что у так называемого динамического массива нет типа массива.

Инициализация массива динамически созданных объектов

Зарезервированные оператором new объекты (будь то одиночные или их массивы) инициализируются по умолчанию. Для инициализации элементов массива по умолчанию (см. раздел 3.3.1) за размером следует расположить пару круглых скобок:

int *pia = new int[10];    // блок из десяти неинициализированных

                           // целых чисел

int *pia2 = new int[10](); // блок из десяти целых чисел,

                           // инициализированных по умолчанию

                           //  значением 0

string *psa = new string[10];    // блок из десяти пустых строк

string *psa2 = new string[10](); // блок из десяти пустых строк

По новому стандарту можно также предоставить в скобках список инициализаторов элементов:

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

// инициализатором

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};

// блок из десяти строк; первые четыре инициализируются заданными

// инициализаторами, остальные элементы инициализируются значением

// по умолчанию

string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};

При списочной инициализации объекта типа встроенного массива (см. раздел 3.5.1) инициализаторы используются для инициализации первых элементов массива. Если инициализаторов меньше, чем элементов, остальные инициализируются значением по умолчанию. Если инициализаторов больше, чем элементов, оператор new потерпит неудачу, не зарезервировав ничего. В данном случае оператор new передает исключение типа bad_array_new_length. Подобно исключению bad_alloc, этот тип определен в заголовке new.

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

Динамическое резервирование пустого массива вполне допустимо

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

size_t n = get_size(); // get_size() возвращает количество необходимых

                       // элементов

int* p = new int[n];   // резервирует массив для содержания элементов

for (int* q = p; q != p + n; ++q)

 /* обработка массива */ ;

Возникает интересный вопрос: что будет, если функция get_size() возвратит значение 0? Этот код сработает прекрасно. Вызов функции new[n] при n равном 0 вполне допустим, даже при том, что нельзя создать переменную типа массива размером 0:

char arr[0]; // ошибка: нельзя определить массив нулевой длины

char *cp = new char[0]; // ok: но обращение к значению cp невозможно

При использовании оператора new для резервирования массива нулевого размера он возвращает допустимый, а не нулевой указатель. Этот указатель гарантированно будет отличен от любого другого указателя, возвращенного оператором new. Он будет подобен указателю на элемент после конца (см. раздел 3.5.3) для нулевого элемента массива. Этот указатель можно использовать теми способами, которыми используется итератор после конца. Его можно сравнивать в цикле, как выше. К нему можно добавить нуль (или вычесть нуль), такой указатель можно вычесть из себя, получив в результате нуль. К значению такого указателя нельзя обратиться, в конце концов, он не указывает на элемент.

В гипотетическом цикле, если функция get_size() возвращает 0, то n также равно 0. Вызов оператора new зарезервирует нуль объектов. Условие оператора for будет ложно (p равно q + n, поскольку n равно 0). Таким образом, тело цикла не выполняется.

Освобождение динамических массивов

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

delete p;     // p должен указывать на динамически созданный объект или

              // быть нулевым

delete [] pa; // pa должен указывать на динамически созданный

              // объект или быть нулевым

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

При применении оператора delete к указателю на массив пустая пара квадратных скобок необходима: она указывает компилятору, что указатель содержит адрес первого элемента массива объектов. Если пропустить скобки оператора delete для указателя на массив (или предоставить их, передав оператору delete указатель на объект), то его поведение будет непредсказуемо.

Напомним, что при использовании псевдонима типа, определяющего тип массива, можно зарезервировать массив без использования [] в операторе new. Но даже в этом случае нужно использовать скобки при удалении указателя на этот массив:

typedef int arrT[42]; // arrT имя типа массив из 42 целых чисел

int *p = new arrT; // резервирует массив из 42 целых чисел; p указывает

                   // на первый элемент

delete [] p;          // скобки необходимы, поскольку был

                      // зарезервирован массив

Несмотря на внешний вид, указатель p указывает на первый элемент массива объектов, а не на отдельный объект типа arrT. Таким образом, при удалении указателя p следует использовать [].

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

Интеллектуальные указатели и динамические массивы

Библиотека предоставляет версию указателя unique_ptr, способную контролировать массивы, зарезервированные оператором new. Чтобы использовать указатель unique_ptr для управления динамическим массивом, после типа объекта следует расположить пару пустых скобок:

// up указывает на массив из десяти неинициализированных целых чисел

unique_ptr up(new int[10]);

up.release(); // автоматически использует оператор delete[] для

              // удаления указателя

Скобки в спецификаторе типа () указывают, что указатель up указывает не на тип int, а на массив целых чисел. Поскольку указатель up указывает на массив, при удалении его указателя автоматически используется оператор delete[].

Указатели unique_ptr на массивы предоставляют несколько иные функции, чем те, которые использовались в разделе 12.1.5. Эти функции описаны в табл. 12.6. Когда указатель unique_ptr указывает на массив, нельзя использовать точечный и стрелочный операторы доступа к элементам. В конце концов, указатель unique_ptr указывает на массив, а не на объект, поэтому эти операторы были бы бессмысленны. С другой стороны, когда указатель unique_ptr указывает на массив, для доступа к его элементам можно использовать оператор индексирования:

for (size_t i = 0; i != 10; ++i)

 up[i] = i; // присвоить новое значение каждому из элементов

Таблица 12.6. Функции указателя unique_ptr на массив

Операторы доступа к элементам (точка и стрелка) не поддерживаются указателями unique_ptr на массивы. Другие его функции неизменны
unique_ptr<T[]> u u может указывать на динамически созданный массив типа T
unique ptr<T[]> u(p) u указывает на динамически созданный массив, на который указывает встроенный указатель p . Тип указателя p должен допускать приведение к типу T (см. раздел 4.11.2). Выражение u[i] возвратит объект в позиции i массива, которым владеет указатель u . u должен быть указателем на массив

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

// чтобы использовать указатель shared_ptr, нужно предоставить

// функцию удаления

shared_ptr sp(new int[10], [](int *p) { delete[] p; });

sp.reset(); // использует предоставленное лямбда-выражение, которое в

// свою очередь использует оператор delete[] для освобождения массива

Здесь лямбда-выражение (см. раздел 10.3.2), использующее оператор delete[], передается как функция удаления.

Если не предоставить функции удаления, результат выполнения этого кода непредсказуем. По умолчанию указатель shared_ptr использует оператор delete для удаления объекта, на который он указывает. Если объект является динамическим массивом, то при использовании оператора delete возникнут те же проблемы, что и при пропуске [], когда удаляется указатель на динамический массив (см. раздел 12.2.1).

Поскольку указатель shared_ptr не поддерживает прямой доступ к массиву, для обращения к его элементам применяется следующий код:

// shared_ptr не имеет оператора индексирования и не поддерживает

// арифметических действий с указателями

for (size_t i = 0; i != 10; ++i)

 *(sp.get() + i) = i; // для доступа к встроенному указателю

                      // используется функция get()

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

Упражнения раздела 12.2.1

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

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

Упражнение 12.25. С учетом следующего оператора new, как будет удаляться указатель pa?

int *pa = new int[10];

 

12.2.2. Класс

allocator

Важный аспект, ограничивающий гибкость оператора new, заключается в том, что он объединяет резервирование памяти с созданием объекта (объектов) в этой памяти. Точно так же оператор delete объединяет удаление объекта с освобождением занимаемой им памяти. Обычно объединение инициализации с резервированием — это именно то, что и нужно при резервировании одиночного объекта. В этом случае почти наверняка известно значение, которое должен иметь объект.

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

Зачастую объединение резервирования и создания оказывается расточительным. Например:

string *const p = new string[n]; // создает n пустых строк

string s;

string *q = p; // q указывает на первую строку

while (cin >> s && q != p + n)

 *q++ = s; // присваивает новое значение *q

const size_t size = q - p; // запомнить количество прочитанных строк

// использовать массив

delete[] p; // p указывает на массив; не забыть использовать delete[]

Этот оператор new резервирует и инициализирует n строк. Но n строк может не понадобиться, — вполне может хватить меньшего количества строк. В результате, возможно, были созданы объекты, которые никогда не будут использованы. Кроме того, тем из объектов, которые действительно используются, новые значения присваиваются немедленно, поверх только что инициализированных строк. Используемые элементы записываются дважды: сначала, когда им присваивается значение по умолчанию, а затем, когда им присваивается значение.

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

Класс allocator и специальные алгоритмы

Библиотечный класс allocator, определенный в заголовке memory, позволяет отделить резервирование от создания. Он обеспечивает не типизированное резервирование свободной области память. Операции, поддерживаемые классом allocator, приведены в табл. 12.7. Операции с классом allocator описаны в этом разделе, а типичный пример его использования — в разделе 13.5.

Подобно типу vector, тип allocator является шаблоном (см. раздел 3.3). Чтобы определить экземпляр класса allocator, следует указать тип объектов, которые он сможет резервировать. Когда объект allocator резервирует память, он обеспечивает непрерывное хранилище соответствующего размера для содержания объектов заданного типа:

allocator alloc; // объект, способный резервировать строки

auto const p = alloc.allocate(n); // резервирует n незаполненных строк

Этот вызов функции allocate() резервирует память для n строк.

Таблица 12.7. Стандартный класс allocator и специальные алгоритмы

allocator<T> a Определяет объект а класса allocator , способный резервировать память для объектов типа T
a.allocate(n) Резервирует пустую область памяти для содержания n объектов типа T
a.deallocate(p, n) Освобождает область памяти, содержавшую n объектов типа T , начиная с адреса в указателе p типа Т* . Указатель p должен быть ранее возвращен функцией allocate() , а размер n — соответствовать запрошенному при создании указателя p . Функцию destroy() следует выполнить для всех объектов, созданных в этой памяти, прежде, чем вызвать функцию deallocate()
a.construct(p, args) Указатель p на тип T должен указывать на незаполненную область памяти; аргументы args передаются конструктору типа Т , используемому для создания объекта в памяти, на которую указывает указатель p
a.destroy(p) Выполняет деструктор (см. раздел 12.1.1) для объекта, на который указывает указатель p типа Т*

Класс allocator резервирует незаполненную память

Память, которую резервирует объект класса allocator, не заполнена. Эта область памяти используется при создании объектов. В новой библиотеке функция-член construct() получает указатель и любое количество дополнительных аргументов; она создает объекты в заданной области памяти. Для инициализации создаваемого объекта используются дополнительные аргументы. Подобно аргументам функции make_shared() (см. раздел 12.1.1), эти дополнительные аргументы должны быть допустимыми инициализаторами объекта создаваемого типа. В частности, если типом объекта является класс, эти аргументы должны соответствовать конструктору этого класса:

auto q = p; // q указывает на следующий элемент после последнего

            // созданного

alloc.construct(q++);          // *q - пустая строка

alloc.construct(q++, 10, 'c'); // *q - cccccccccc

alloc.construct(q++, "hi");    // *q - hi!

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

Использование незаполненной области памяти, в которой еще не был создан объект, является ошибкой:

cout << *p << endl; // ok: использует оператор вывода класса string

cout << *q << endl; // ошибка: q указывает на незаполненную память!

Чтобы использовать память, возвращенную функцией allocate(), в ней следует создать объекты. Результат использования незаполненной памяти другими способами непредсказуем.

По завершении использования объектов следует удалить ранее созданные элементы. Для этого следует вызвать функцию destroy() каждого созданного элемента. Функция destroy() получает указатель и запускает деструктор (см. раздел 12.1.1) указанного объекта:

while (q != p)

 alloc.destroy(--q); // освободить фактически зарезервированные строки

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

Удалять можно только те элементы, которые были фактически созданы.

Как только элементы удалены, память можно повторно использовать для содержания других строк или возвратить их операционной системе. Для освобождения памяти используется функция deallocate():

alloc.deallocate(p, n);

Указатель, передаваемый функции deallocate(), не может быть нулевым; он должен указывать на область памяти, зарезервированной функцией allocate(). Кроме того, переданный ей аргумент размера должен совпадать с размером, использованным при вызове функции allocate(), зарезервировавшим область памяти, на которую указывает указатель.

Алгоритмы копирования и заполнения неинициализированной памяти

В дополнение к классу allocator библиотека предоставляет два алгоритма, способных создавать объекты в неинициализированной памяти. Эти функции описаны в табл. 12.8 и определены в заголовке memory.

Таблица 12.8. Алгоритмы, связанные с классом allocator

Эти функции создают элементы по назначению, а не присваивают их
uninitialized_copy(b, е, b2) Копирует элементы из исходного диапазона, обозначенного итераторами b и е , в незаполненную память, обозначенную итератором b2 . Память, обозначенная итератором b2 , должна быть достаточно велика для содержания копии элементов из исходного диапазона
uninitialized_copy_n(b, n, b2) Копирует n элементов, начиная с обозначенного итератором b в незаполненную память, начиная с позиции b2
uninitialized_fill(b, е, t) Создает объекты в диапазоне незаполненной памяти, обозначенной итераторами b и е как копию t
uninitialized_fill_n(b, n, t) Создает n объектов, начиная с b . Итератор b должен обозначать незаполненную память достаточного размера для содержания заданного количества объектов

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

// зарезервировать вдвое больше элементов, чем хранения в vi

auto p = alloc.allocate(vi.size() * 2);

// создать элементы, начиная с p как копии элементов в vi

auto q = uninitialized_copy(vi.begin(), vi.end(), p);

// инициализировать остальные элементы значением 42

uninitialized_fill_n(q, vi.size(), 42);

Подобно алгоритму copy() (см. раздел 10.2.2), алгоритм uninitialized_copy() получает три итератора. Первые два обозначают исходную последовательность, а третий обозначает получателя, в который будут скопированы эти элементы. Итератор назначения, переданный алгоритму uninitialized_copy(), должен обозначить незаполненную память. В отличие от алгоритма copy(), алгоритм uninitialized_copy() создает элементы в своем получателе.

Подобно алгоритму copy(), алгоритм uninitialized_copy() возвращает (приращенный) итератор назначения. Таким образом, вызов функции uninitialized_copy() возвращает указатель на следующий элемент после последнего заполненного. В данном примере этот указатель сохраняется в переменной q, передаваемой функции uninitialized_fill_n(). Эта функция, как и функция fill_n() (см. раздел 10.2.2), получает указатель на получателя, количество и значение. Она создает заданное количество объектов из заданного значения в позиции, начиная с заданной получателем.

Упражнения раздела 12.2.2

Упражнение 12.26. Перепишите программу из начала раздела, используя класс allocator.

 

12.3. Использование библиотеки: программа запроса текста

 

Для завершения обсуждения библиотеки реализуем простую программу текстового запроса. Она позволит пользователю искать в заданном файле слова, которые могли встречаться в нем. Результатом запроса будет количество экземпляров слова и список строк, в которых оно присутствует. Если слово встречается несколько раз в той же строке, то она отображается только однажды. Строки отображаются в порядке возрастания, т.е. строка номер 7 отображается перед строкой номер 9 и т.д.

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

element occurs 112 times

 (line 36) A set element contains only a key;

 (line 158) operator creates a new element

 (line 160) Regardless of whether the element

 (line 168) When we fetch an element from a map, we

 (line 214) If the element is not found, find returns

Далее следует примерно 100 строк, также содержащих слово element.

 

12.3.1. Проект программы

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

• Читая ввод, программа должна запоминать строку (строки), в которой присутствует искомое слово. Следовательно, программа должна читать ввод построчно и разделять прочитанные строки на отдельные слова

• При создании вывода программа должна:

  • получать номера строк, содержащих искомое слово;

  • нумеровать строки в порядке возрастания без дубликатов;

  • отображать текст исходного файла по заданному номеру строки.

Эти требования можно выполнить с помощью библиотечных средств.

• Для хранения копии всего входного файла используем вектор vector. Каждая строка входного файла станет элементом этого вектора. При необходимости вывода строку можно будет выбрать, используя ее номер как индекс.

• Для разделения строки на слова используем строковый поток istringstream (см. раздел 8.3).

• Для хранения номеров строк, в которых присутствует искомое слово, используем контейнер set. Это гарантирует, что каждая строка будет присутствовать только однажды, а номера строки будут храниться в порядке возрастания.

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

По рассматриваемым вскоре причинам в решении будет также использован указатель shared_ptr.

Структуры данных

Несмотря на то что программу можно было бы написать, используя контейнеры vector, set и map непосредственно, полезней определить более абстрактное решение. Для начала разработаем класс для содержания входного файла, чтобы упростить запрос. Этот класс, TextQuery, будет содержать вектор и карту. Вектор будет содержать текст входного файла, а карта ассоциировать каждое слово в этом файле с набором номеров строк, в которых присутствует искомое слово. У этого класса будет конструктор, читающий заданный входной файл, и функция, обрабатывающая запросы.

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

Проще всего вернуть все эти данные, определив второй класс, назовем его QueryResult, который и будет содержать результаты запроса. У этого класса будет функция print(), выводящая результаты, хранимые в объекте класса QueryResult.

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

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

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

Избежать копирования можно, возвратив итераторы (или указатели) на содержимое объекта TextQuery. Но с этим подходом связана проблема: что если объект класса TextQuery будет удален до объекта класса QueryResult? В этом случае объект класса QueryResult ссылался бы на данные в больше не существующем объекте.

Это требует синхронизации продолжительности существования объекта класса QueryResult с объектом класса TextQuery, результаты которого он представляет. Таким образом, эти два класса концептуально "совместно используют" данные. Для отражения совместного использования этих структур данных используем указатели shared_ptr (см. раздел 12.1.1).

Применение класса TextQuery

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

void runQueries(ifstream &infile) {

 // infile - поток ifstream для входного файла

 TextQuery tq(infile); // хранит файл и строит карту запроса

 // цикл взаимодействия с пользователем: приглашение ввода искомого

 // слова и вывод результатов

 while (true) {

  cout << "enter word to look for, or q to quit: ";

  string s;

  // остановиться по достижении конца файла или при встрече

  // символа 'q' во вводе

  if (!(cin >> s) || s == "q") break;

  // выполнить запрос и вывести результат

  print(cout, tq.query(s)) << endl;

 }

}

Начнем с инициализации объекта tq класса TextQuery данными из переданного потока ifstream. Конструктор TextQuery() читает файл в свой вектор и создает карту, связывающую слова из ввода с номерами строк, в которых они присутствуют.

Цикл while (непрерывно) запрашивает у пользователя искомое слово и выводит полученные результаты. Условие цикла проверяет литерал true (см. раздел 2.1.3), поэтому оно всегда истинно. Для выхода из цикла используется оператор break (см. раздел 5.5.1) в операторе if. Он проверяет успешность чтения. Если оно успешно, проверяется также, не ввел ли пользователь символ "q", желая завершить работу. Получив искомое слово, запрашиваем его поиск у объекта tq, а затем вызываем функцию print() для вывода результата поиска.

Упражнения раздела 12.3.1

Упражнение 12.27. Классы TextQuery и QueryResult используют только те возможности, которые уже были описаны ранее. Не заглядывая вперед, напишите собственные версии этих классов.

Упражнение 12.28. Напишите программу, реализующую текстовые запросы, не определяя классы управления данными. Программа должна получать файл и взаимодействовать с пользователем, запрашивая слова, искомые в этом файле. Используйте контейнеры vector, map и set для хранения данных из файла и создания результатов запросов.

Упражнение 12.29. Перепишите цикл взаимодействия с пользователем, используя цикл do while (см. раздел 5.4.4). Объясните, какая версия предпочтительней и почему.

 

12.3.2. Определение классов программы запросов

Начнем с определения класса TextQuery. Пользователь создает объекты этого класса, предоставляя поток istream для чтения входного файла. Этот класс предоставляет также функцию query(), которая получает строку и возвращает объект класса QueryResult, представляющий строки, в которых присутствует искомое слово.

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

Чтобы сделать код немного понятней, определим также тип-член (см. раздел 7.3.1) для обращения к номерам строк, которые являются индексами вектора строк:

class QueryResult; // объявление необходимого типа возвращаемого

                   //  значения функции запроса

class TextQuery {

public:

 using line_no = std::vector::size_type;

 TextQuery(std::ifstream&);

 QueryResult query(const std::string&) const;

private:

 std::shared_ptr> file; // исходный файл

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

 // это слово

 std::map

          std::shared_ptr>> wm;

};

Самая трудная часть этого кода — разобраться в именах классов. Как обычно, для кода из файла заголовка применяется часть std::, указывающая имя библиотеки (см. раздел 3.1). Но в данном случае частое повторение имени std:: делает код немного менее понятным для чтения. Рассмотрим пример:

std::map>> wm;

Его будет проще понять, переписав так:

map>> wm;

Конструктор TextQuery()

Конструктор TextQuery() получает поток ifstream, позволяющий читать строки по одной:

// прочитать входной файл, создать карту строк и их номеров

TextQuery::TextQuery(ifstream &is): file(new vector) {

 string text;

 while (getline(is, text)) { // для каждой строки в файле

  file->push_back(text);     // запомнить эту строку текста

  int n = file->size() - 1;  // номер текущий строки

  istringstream line(text);  // разделить строку на слова

  string word;

  while (line >> word) { // для каждого слова в этой строке

   // если слова еще нет в wm, индексация добавляет новый

   // элемент

   auto &lines = wm[word]; // lines - это shared_ptr

   if (!lines) // этот указатель - вначале нулевой, когда

               // встречается слово

    lines.reset(new set); // резервирует новый набор

   lines->insert(n); // вставить номер этой строки

  }

 }

}

Список инициализации конструктора резервирует новый вектор для содержания текста из входного файла. Функция getline() используется для чтения из файла по одной строке за раз и их помещения в вектор. Поскольку file — это указатель shared_ptr, используем оператор -> для обращения к его значению, чтобы вызвать функцию push_back() для того элемента вектора, на который указывает указатель file.

Затем поток istringstream (см. раздел 8.3) используется для обработки каждого слова только что прочитанной строки. Внутренний цикл while использует оператор ввода класса istringstream для чтения каждого слова текущей строки в строку word. В цикле while используется оператор индексирования карты для доступа к связанному со словом указателю shared_ptr и связи ссылки lines с этим указателем. Обратите внимание, что lines — это ссылка, поэтому внесенные в нее изменения будут сделаны с элементом карты wm.

Если слова еще нет в карте, оператор индексирования добавит строку word в карту wm (см. раздел 11.3.4). Ассоциируемый со строкой word элемент инициализирован значением по умолчанию. Это значит, что ссылка lines будет нулевой, если оператор индексирования добавит строку word в карту wm. Если ссылка lines будет нулевой, резервируем новый набор и вызываем функцию reset() для обновлении указателя shared_ptr, на который ссылается ссылка lines, чтобы он указывал на этот только что созданный набор.

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

Класс QueryResult

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

class QueryResult {

 friend std::ostream& print(std::ostream&, const QueryResult&);

public:

 QueryResult(std::string s,

             std::shared_ptr> p,

             std::shared_ptr> f):

  sought(s), lines(p), file(f) { }

private:

 std::string sought; // слово, представляющее запрос

 std::shared_ptr> lines;       // номера строк

 std::shared_ptr> file; // входной файл

};

Единственная задача конструктора — сохранить свои аргументы в соответствующих переменных-членах, что он и делает в списке инициализации конструктора (см. раздел 7.1.4).

Функция query()

Функция query() получает строку, которую она использует для поиска соответствующего набора номеров строк в карте. Если строка найдена, функция query() создает объект класса QueryResult из заданной строки, переменной-члена file класса TextQuery и набора, извлеченного из карты wm.

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

QueryResult

TextQuery::query(const string &sought) const {

 // возвратить указатель на этот набор, если искомое слово не найдено

 static shared_ptr> nodata(new set);

 // использовать find() но не индексировать, чтобы избежать

 // добавления слова в карту wm!

 auto loc = wm.find(sought);

 if (loc == wm.end())

  return QueryResult(sought, nodata, file); // не найдено

 else

  return QueryResult(sought, loc->second, file);

}

Вывод результатов

Функция print() выводит заданный объект класса QueryResult в заданный поток:

ostream &print(ostream &os, const QueryResult &qr) {

 // если слово найдено, вывести количество и все вхождения

 os << qr.sought << " occurs " << qr.lines->size() << " "

    << make_plural(qr.lines->size(), "time", "s") << endl;

 // вывести каждую строку, в которой присутствует слово

 for (auto num : *qr.lines) // для каждого элемента в наборе

  // не путать пользователя с номерами строк, начинающимися с 0

  os << "\t (line " << num + 1 << ") "

     << *(qr.file->begin() + num) << endl;

 return os;

}

Для отчета о количестве найденных соответствий используем функцию size() набора, на который ссылается qr.lines. Поскольку этот набор контролируется указателем shared_ptr, следует помнить об обращении к значению lines. Чтобы вывести слово time или times, в зависимости от того, равен ли размер 1, используем функцию make_plural() (см. раздел 6.3.2).

Цикл for перебирает набор, на который ссылается lines. Тело цикла for выводит номер строки, откорректированный так, как привычно человеку. Числа в наборе являются индексами элементов в векторе, их нумерация начинается с нуля. Но большинство пользователей привыкли к тому, что первая строка имеет номер 1, поэтому будем систематически добавлять 1 к номерам строк, чтобы отображать их в общепринятой форме.

Используем номер строки для выбора строк из вектора, на который указывает указатель-член file. Помните, что при добавлении числа к итератору будет получен элемент на столько же элементов далее (см. раздел 3.4.2). Таким образом, часть file->begin() + num дает номер элемента от начала вектора, на который указывает file.

Обратите внимание: эта функция правильно обрабатывает случай, когда слово не найдено. В данном случае набор будет пуст. Первый оператор вывода заметит, что слово встретилось нуль раз. Поскольку *res.lines пуст, цикл for не выполнится ни разу.

Упражнения раздела 12.3.2

Упражнение 12.30. Определите собственные версии классов TextQuery и QueryResult, а также выполните функцию runQueries() из раздела 12.3.1.

Упражнение 12.31. Что будет, если для хранения номеров строк использовать вектор вместо набора? Какой подход лучше? Почему?

Упражнение 12.32. Перепишите классы TextQuery и QueryResult так, чтобы для хранения входного файла вместо вектора vector использовался класс StrBlob.

Упражнение 12.33. В главе 15 программа запроса будет дополнена, и классу QueryResult понадобятся дополнительные члены. Добавьте функции-члены по имени begin() и end(), возвращающие итераторы для набора номеров строк, возвращенных данным запросом, и функцию-член get_file(), возвращающую указатель shared_ptr на файл в объекте QueryResult.

 

Резюме

В языке С++ для резервирования памяти используется оператор new, а для освобождения — оператор delete. Библиотека определяет также класс allocator, чтобы резервировать пустые блоки динамической памяти.

Резервирующие динамическую память программы отвечают за ее освобождение. Освобождение динамической памяти — богатейший источник ошибок: память может быть не освобождена никогда или может быть освобождена, когда еще есть указатели на нее. Новая библиотека определяет классы интеллектуальных указателей (shared_ptr, unique_ptr и weak_ptr), делающие работу с динамической памятью намного более безопасной. Интеллектуальный указатель автоматически освобождает память, как только удаляется последний указатель на нее. В современных программах С++ следует использовать именно интеллектуальные указатели.

 

Термины

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

Динамическая память (free store). Пул памяти, доступный программе для хранения объектов, создаваемых динамически.

Динамически созданный объект (dynamically allocated object). Объект, который создан в динамической памяти. Такие объекты существуют до тех пор, пока не будут удалены из динамической памяти явно или пока программа не завершит работу.

Интеллектуальный указательshared ptr. Интеллектуальный указатель, обеспечивающий совместную собственность: объект освобождается, когда удаляется последний указатель shared_ptr на тот объект.

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

Интеллектуальный указательweak_ptr. Интеллектуальный указатель на объект, управляемый указателем shared_ptr. Указатель shared_ptr не учитывает указатель weak_ptr, принимая решение об освобождении своего объекта.

Интеллектуальный указатель (smart pointer). Библиотечный тип, действует как указатель, но допускающий проверку на безопасность использования. Тип сам заботится об освобождении памяти, когда это нужно.

Классallocator. Библиотечный класс, резервирующий области памяти.

Операторdelete. Освобождает участок памяти, зарезервированный оператором new. Оператор delete p освобождает объект, a delete [] p — массив, на который указывает указатель p. Указатель p может быть пуст или указывать на область память, зарезервированную оператором new.

Операторnew. Резервирует область в динамической памяти. Оператор new T резервирует область памяти, создает в ней объект типа T и возвращает указатель на этот объект. Если T — тип массива, оператор new возвращает указатель на его первый элемент. Аналогично оператор new [n] Т резервирует n объектов типа T и возвращает указатель на первый элемент массива. По умолчанию вновь созданный объект инициализируется значением по умолчанию. Но можно также предоставить инициализаторы.

Потерянный указатель (dangling pointer). Указатель, содержащий адрес области памяти, в которой уже нет объекта. Потерянный указатель является весьма распространенным источником ошибок в программе, причем такие ошибки крайне трудно обнаружить.

Размещающий операторnew (placement new). Форма оператора new, получающая в круглых скобках дополнительные аргументы после ключевого слова new; например, синтаксис new (nothrow) int устанавливает, что оператор new не должен передавать исключения.

Распределяемая память (heap). То же, что и динамическая память.

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

Функция удаления (deleter). Функция, передаваемая интеллектуальному указателю, для использования вместо оператора delete при освобождении объекта, на который указывает указатель.