Книга: Эффективное использование STL
Совет 30. Следите за тем, чтобы приемный интервал имел достаточный размер
Совет 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
записывает результаты в приемный интервал по одному элементу, и с этим ничего не поделаешь.
При вставке в контейнеры vector
и string
для сокращения затрат можно последовать совету 14 и заранее вызвать reserve
. Затраты на сдвиг элементов при каждой вставке от этого не исчезнут, но по крайней мере вы избавитесь от необходимости перераспределения памяти контейнера:
vector<int> values; // См. Ранее
vector<int> results;
…
results.reserve(results.size()+values.size()); // Обеспечить наличие
// в векторе results
// емкости для value.size()
// элементов
transform(values.begin(), values.end(), // То же, что и ранее,
inserter(results, results.begin()+results.size()/2), // но без лишних
transmogrify); // перераспределений памяти
При использовании функции reserve
для повышения эффективности серии вставок всегда помните, что reserve
увеличивает только емкость контейнера, а размер остается неизменным. Даже после вызова reserve
при работе с алгоритмом, который должен включать новые элементы в vector
или string
, необходимо использовать итератор вставки (то есть итератор, возвращаемый при вызове back_inserter
, front_inserter
или inserter
).
Чтобы это стало абсолютно ясно, рассмотрим ошибочный путь повышения эффективности для примера, приведенного в начале совета (с присоединением результатов обработки элементов values
к results
):
vector<int> values; // См. ранее
vector<int> results;
…
results.reserve(results.size()+values.size()); // См. ранее
transform(values.begin(), values.end(), // Результаты вызова
results.end(), // transmogrify записываются
transmogrify); // в неинициализированную
// память; последствия
// непредсказуемы!
В этом фрагменте transform
в блаженном неведении пытается выполнить присваивание в неинициализированной памяти за последним элементом results
. Обычно подобные попытки приводят к ошибкам времени выполнения, поскольку операция присваивания имеет смысл лишь для двух объектов, но не между объектом и двоичным блоком с неизвестным содержимым. Но даже если этот код каким-то образом справится с задачей, вектор results
не будет знать о новых «объектах», якобы созданных в его неиспользуемой памяти. С точки зрения results
вектор после вызова transform
сохраняет прежний размер, а его конечный итератор будет указывать на ту же позицию, на которую он указывал до вызова transform
. Мораль? Использование reserve
без итератора вставки приводит к непредсказуемым последствиям внутри алгоритмов и нарушению целостности данных в контейнере.
В правильном решении функция reserve
используется в сочетании с итератором вставки:
vector<int> values; // См. ранее
vector<int> results;
…
results.reserve(results.size()+values.size()); // См. ранее
transform(values.begin(), values.end(), // Результаты вызова
back_inserter(results), // transmogrify записываются
transmogrify); // в конец вектора results
// без лишних перераспределений
// памяти
До настоящего момента предполагалось, что алгоритмы (такие как transform
) записывают результаты своей работы в контейнер в виде новых элементов. Эта ситуация является наиболее распространенной, но иногда новые данные требуется записать поверх существующих. В таких случаях итератор вставки не нужен, но вы должны в соответствии с данным советом проследить за тем, чтобы приемный интервал был достаточно велик.
Допустим, вызов transform
должен записывать результаты в results
поверх существующих элементов. Если количество элементов в results
не меньше их количества в values
, задача решается просто. В противном случае придется либо воспользоваться функцией resize
для приведения results
к нужному размеру:
vector<int> results;
…
if (results.size() < values.size()) { // Убедиться в том, что размер
results.resize(values.size()); // results по крайней мере
} // не меньше размера values
transform(values,begin(), values.end(), // Перезаписать первые
back_inserter(results), // values.size() элементов results
transmogrify);
либо очистить results
и затем использовать итератор вставки стандартным способом:
…
results.clear(); // Удалить из results все элементы
results.reserve(values.size()); // Зарезервировать память
transform(values.begin(), values.end(), // Занести выходные данные
back_inserter(results), // transform в results
transmogrify);
В данном совете было продемонстрировано немало вариаций на заданную тему, но я надеюсь, что в памяти у вас останется основная мелодия. Каждый раз, когда вы используете алгоритм, требующий определения приемного интервала, позаботьтесь о том, чтобы приемный интервал имел достаточные размеры или автоматически увеличивался во время работы алгоритма. Второй вариант реализуется при помощи итераторов вставки — таких, как ostream_iterator
, или возвращаемых в результате вызова back_inserter
, front_inserter
и inserter
. Вот и все, о чем необходимо помнить.
- Совет 30. Следите за тем, чтобы приемный интервал имел достаточный размер
- Совет 31. Помните о существовании разных средств сортировки
- Совет 32. Сопровождайте вызовы remove-подобных алгоритмов вызовом erase
- Совет 33. Будьте внимательны при использовании remove-подобных алгоритмов с контейнерами указателей
- Совет 34. Помните о том. какие алгоритмы получают сортированные интервалы
- Совет 35. Реализуйте простые сравнения строк без учета регистра символов с использованием mismatch или lexicographical_compare
- Совет 36. Правильно реализуйте copy_if
- Совет 37. Используйте accumulate или for_each для обобщения интервальных данных
- Восстановление из резервной копии на системе-приемнике
- Неисправности акустических систем
- Размер страницы базы данных
- Особенности системы защиты данных в InterBase
- Система безопасности InterBase
- Что делать, если при установке принтера появляется сообщение Невозможно завершение операции. Подсистема печати недоступн...
- Уменьшение размера, занимаемого индексами
- Системные переменные ROWS_AFFECTED, GDSCODE, SQLCODE, TRANSACTIONJD, CONNECTIONJD
- Системное программное обеспечение
- Хранение конфигурации в системном реестре
- Модификация системных таблиц
- 7 Система Цикл: долгосрочные цели