В этой главе я постараюсь решить две основные задачи. Во-первых, я представлю некоторые малоизвестные алгоритмы и покажу, как с их помощью упростить себе жизнь. Не беспокойтесь, вам не придется запоминать длинные списки имен. Алгоритмы, представленные в этой главе, предназначены для решения повседневных задач — сравнение строк без учета регистра символов, эффективный поиск
n
объектов, в наибольшей степени соответствующих заданному критерию, обобщение характеристик всех объектов в заданном интервале и имитация
copy_if
(алгоритм из исходной реализации HP STL, исключенный в процессе стандартизации).
Во-вторых, я научу вас избегать стандартных ошибок, возникающих при работе с алгоритмами. Например, при вызове алгоритма remove и его родственников
remove_if
и
unique
необходимо точно знать, что эти
алгоритмы делают (и чего они не делают). Данное правило особенно актуально при вызове
remove
для интервала, содержащего указатели. Многие алгоритмы работают только с отсортированными интервалами, и программист должен понимать, что это за алгоритмы и почему для них установлено подобное ограничение. Наконец, одна из наиболее распространенных ошибок, допускаемых при работе с алгоритмами, заключается в том, что программист предлагает алгоритму записать результаты своей работы в несуществующую область памяти. Я покажу, как это происходит и как предотвратить эту ошибку.
Возможно, к концу главы вы и не будете относиться к алгоритмам с тем же энтузиазмом, с которым обычно относятся к контейнерам, но по крайней мере будете чаще применять их в своей работе.
Совет 30. Следите за тем, чтобы приемный интервал имел достаточный размер
Контейнеры STL автоматически увеличиваются с добавлением новых объектов (функциями
insert
,
push_front
,
push_back
и т. д.). Автоматическое изменение размеров чрезвычайно удобно, и у многих программистов создается ложное впечатление, что контейнер сам обо всем позаботится и им никогда не придется следить за наличием свободного места. Если бы так!
Проблемы возникают в ситуации, когда программист думает о вставке объектов в контейнер, но не сообщает о своих мыслях STL. Типичный пример:
int transmogrify(int х); // Функция вычисляет некое новое значение
// по переданному параметру х
vector<int> values;
… // Заполнение вектора values данными
vector<int> results; // Применить transmogrify к каждому объекту
transform(values.begin, // вектора values и присоединить возвращаемые
values.end, // значения к results.
results.end, // Фрагмент содержит ошибку!
transmogrify);
В приведенном примере алгоритм
transform
получает информацию о том, что приемный интервал начинается с
results.end
. С этой позиции он и начинает вывод значений, полученных в результате вызова
transmogrify
для каждого элемента
values
. Как и все алгоритмы, использующие приемный интервал,
transform
записывает свои результаты, присваивая значения элементам заданного интервала. Таким образом,
transform
вызовет
transmogrify
для
values[0]
и присвоит результат
*results.end
. Затем функция
transmogrify
вызывается для
values[1]
с присваиванием результата
*(results.end+1)
. Происходит катастрофа, поскольку в позиции
*results.end
(и тем более в
*(results.end+1))
не существует объекта! Вызов
transform
некорректен из-за попытки присвоить значение несуществующему объекту (в совете 50 объясняется, как отладочная реализация STL позволит обнаружить эту проблему на стадии выполнения).
Допуская подобную ошибку, программист почти всегда рассчитывает на то, что результаты вызова алгоритма будут вставлены в приемный контейнер вызовом
insert
. Если вы хотите, чтобы это произошло, так и скажите. В конце концов, STL — всего лишь библиотека, и читать мысли ей не положено. В нашем примере задача решается построением итератора, определяющего начало приемного интервала, вызовом
back_inserter
:
vector<int> values;
transform(values.begin, // Применить transmogrify к каждому
values.end, // объекту вектора values
back_inserter(results), //
и дописать значения в конец results
transmogrify);
При использовании итератора, возвращаемого при вызове
back_inserter
, вызывается
push_back
, поэтому
back_inserter
может использоваться со всеми контейнерами, поддерживающими
push_back
(то есть со всеми стандартными последовательными контейнерами:
vector
,
string
,
deque
и
list
). Если вы предпочитаете, чтобы алгоритм вставлял элементы в начало контейнера, Воспользуйтесь
front_inserter
. Во внутренней реализации
front_inserter
используется
push_front
, поэтому
front_inserter
работает только с контейнерами, поддерживающими эту функцию (то есть
deque
и
list
).
… //См. ранее
list<int> results; // Теперь используется
// контейнер list
transform(values.begin, values.end, // Результаты вызова transform
front_inserter(results), // вставляются в начало results
transmogrify); //в обратном порядке
Поскольку при использовании
front_inserter
новые элементы заносятся в начало
results
функцией
push_front
, порядок следования объектов в
results
будет обратным по отношению к порядку соответствующих объектов в
values
. Это ишь одна из причин, по которым
front_inserter
используется реже
back_inserter
. Другая причина заключается в том, что
vector
не поддерживает
push_front
, поэтому
front_inserter
не может использоваться с
vector
.
Чтобы результаты
transform
выводились в начале
results
, но с сохранением порядка следования элементов, достаточно перебрать содержимое
values
в обратном порядке:
list<int> results; // См. ранее
…
transform(values.rbegin, values.rend, // Результаты вызова transform
front_inserter(results), // вставляются в начало results
transmogrify); // с сохранением исходного порядка
Итак,
front_inserter
заставляет алгоритмы вставлять результаты своей работы в начало контейнера, a
back_inserter
обеспечивает вставку в конец контейнера. Вполне логично предположить, что
inserter
заставляет алгоритм выводить свои результаты с произвольной позиции:
vector<int> values; // См. ранее
…
vector<int> results; // См. ранее - за исключением того, что
… // results на этот раз содержит данные
// перед вызовом transform.
transform(values.begin, // Результаты вызова transmogrify
values.end, // выводятся в середине results
inserter(results, results, begin+results.size/2),
transmogrify);
Независимо от выбранной формы —
back_inserter, front_inserter
или
inserter
— объекты вставляются в приемный интервал по одному. Как объясняется в совете 5, это может привести к значительным затратам для блоковых контейнеров (
vector
,
string
и
deque
), однако средство, предложенное в совете 5 (интервальные функции), неприменимо в том случае, если вставка выполняется алгоритмом. В нашем примере
transform
записывает результаты в приемный интервал по одному элементу, и с этим ничего не поделаешь.