В практической задаче определите вывод программы и объясните, почему два размера различаются:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 2, 4};
auto new_end = std::remove(v.begin(), v.end(), 2);
std::cout << v.size() << ' ' << std::distance(v.begin(), new_end);
}
Программа выведет 5 3. Алгоритм std::remove перемещает элементы, не равные 2, в начало диапазона и возвращает итератор на новый логический конец, но не изменяет размер контейнера. Чтобы физически удалить хвост из std::vector, нужно передать этот итератор в erase.
Алгоритмы STL работают с парами итераторов и не обязаны знать, как конкретный контейнер хранит элементы или изменяет свой размер. Поэтому std::remove решает только задачу перестановки элементов внутри диапазона, а изменение структуры контейнера оставляется его методам.
Такое разделение позволяет применять алгоритм к разным контейнерам и диапазонам, включая массивы и пользовательские структуры. Однако для последовательных контейнеров оно требует явно выполнить второй шаг — удаление обработанного хвоста.
После вызова std::remove вектор по-прежнему содержит пять элементов. Первые три элемента образуют логически очищенный диапазон: 1, 3, 4, а элементы после new_end остаются существующими, но их значения не следует использовать как часть результата.
Если забыть вызвать erase, в программе останутся лишние элементы. Это может привести к неверному размеру коллекции, ошибкам при последующей обработке и избыточному потреблению памяти.
std::remove проходит по диапазону, находит элементы, которые нужно сохранить, и перемещает их вперед поверх удаляемых. В примере начало вектора становится эквивалентным последовательности 1, 3, 4, а new_end указывает сразу после неё.
Размер std::vector возвращается через v.size() и остаётся равным пяти, потому что алгоритм не вызывает vector::erase. Расстояние от v.begin() до new_end равно трём — это размер нового логического диапазона.
Классический идиоматический вариант выглядит так:
Сначала std::remove выполняет компактирование элементов, затем erase удаляет хвост и уменьшает размер контейнера до трёх. Для контейнеров с методом remove, например std::list, обычно применяют именно этот метод контейнера: он сразу удаляет узлы и не требует идиомы erase-remove.
Важно, что элементы в диапазоне [new_end, v.end()) остаются валидными объектами, но их значения не определены как полезный результат алгоритма. Итераторы и ссылки на элементы, которые были физически удалены вызовом erase, становятся недействительными; для std::vector также могут измениться позиции элементов после места удаления.
В обработчике заказов нужно удалить из вектора все отменённые записи. Рассматривались три варианта: создавать новый вектор с помощью std::copy_if, удалять элементы по одному в цикле или использовать erase-remove.
Создание нового вектора делает код наглядным и сохраняет исходный контейнер, но требует дополнительной памяти. Удаление по одному может приводить к многократному сдвигу оставшихся элементов и иметь квадратичную сложность в худшем случае.
Был выбран erase-remove: алгоритм компактирует элементы за линейное время, а один вызов erase удаляет хвост. Для std::vector это обычно даёт линейную суммарную сложность и не требует дополнительного контейнера, поэтому вариант подходит для массового удаления.
Что именно возвращает std::remove и можно ли разыменовать возвращённый итератор?
Возвращается итератор на новый логический конец диапазона, а не итератор на последний сохранённый элемент. Его можно использовать как границу диапазона и передать в erase, но разыменовывать его нельзя, если он равен last или обозначает позицию после последнего сохранённого элемента.
Какова сложность идиомы erase-remove для std::vector?
std::remove выполняет линейное число сравнений и перемещений. erase удаляет хвост после нового логического конца; в данном случае перед удаляемым хвостом нет элементов, которые нужно дополнительно сдвигать. Поэтому общая сложность линейная, а количество выделенной памяти обычно не меняется.
Почему для std::list предпочтителен list::remove, а не общий std::remove?
std::list::remove удаляет соответствующие узлы непосредственно и корректно обрабатывает структуру связного списка. Общий std::remove лишь перемещает значения присваиваниями в диапазоне, не удаляя узлы; последующий erase возможен, но менее естественен и не использует специализированный механизм списка.