Что на самом деле гарантирует std::vector::shrink_to_fit при попытке освободить неиспользуемую память?
std::vector::shrink_to_fit лишь просит контейнер уменьшить capacity() до значения, близкого к size(). Стандарт не гарантирует, что память действительно будет освобождена. Если при выполнении произойдёт перераспределение памяти, все итераторы, указатели и ссылки на элементы становятся недействительными.
Динамический массив обычно хранит запас свободной ёмкости, чтобы последовательные добавления выполнялись эффективно и не требовали перераспределения памяти на каждой операции. После массового удаления элементов этот запас может стать избыточным, поэтому библиотеке нужен способ выразить намерение уменьшить занимаемую память.
При этом принудительное перераспределение может быть дорогим: элементы придётся переместить или скопировать, а операционная система или распределитель памяти не обязаны немедленно вернуть освобождённый блок системе. Поэтому стандарт описывает shrink_to_fit как необязательный запрос, оставляя решение реализации.
Допустим, вектор временно вырос до большого размера, а затем большая часть элементов была удалена. Его size() уменьшился, но capacity() может остаться прежним, из-за чего контейнер продолжит владеть большим буфером.
Неверно считать, что вызов shrink_to_fit обязательно уменьшит capacity(). Также опасно продолжать использовать сохранённые итераторы или ссылки: реализация может выполнить перераспределение даже тогда, когда программа не ожидает изменения адресов элементов.
Вызов shrink_to_fit имеет семантику необязательного запроса. Реализация может уменьшить ёмкость, а может оставить её без изменений, если считает перераспределение невыгодным или не может выполнить его в текущих условиях.
После вызова size() и значения элементов не меняются. Измениться может только выделенная ёмкость; конкретный результат не является переносимым контрактом программы.
Если перераспределение произошло, недействительными становятся все итераторы, указатели и ссылки на элементы. Если перераспределения не было, они сохраняют действительность. Поэтому нельзя надёжно определить действительность старого итератора только по факту вызова shrink_to_fit; безопаснее считать его потенциально недействительным и получать заново.
Для обычного управления производительностью часто достаточно оставить избыточную ёмкость: это избегает копирования или перемещения элементов. Если освобождение памяти действительно важно, можно создать новый компактный контейнер и заменить старый, но такой подход требует дополнительной временной памяти, может быть дорогим для тяжёлых объектов и также инвалидирует прежние ссылки.
shrink_to_fit не следует использовать как средство гарантированного возврата памяти операционной системе. Он выражает намерение, а не предоставляет строгую гарантию результата.
Сервис формирует большой временный набор данных в std::vector, после обработки оставляет только несколько элементов и затем продолжает долго работать. Команда рассматривает три варианта.
Первый вариант — ничего не делать. Он сохраняет ёмкость для возможного следующего роста и не вызывает перемещения элементов, но большой буфер продолжает удерживаться.
Второй вариант — вызвать shrink_to_fit. Это простой и обычно подходящий компромисс: реализация сможет уменьшить буфер, если это выгодно. Однако освобождение не гарантировано, а все внешние ссылки и итераторы нужно считать потенциально недействительными.
Третий вариант — построить новый вектор из оставшихся элементов и заменить старый. Он лучше подходит, когда необходимо начать владеть компактным буфером, но требует дополнительного выделения памяти и временного хранения, а также может вызвать дорогостоящие операции над элементами.
Если набор данных после обработки больше не должен существенно расти, выбранным решением обычно будет shrink_to_fit с отказом от сохранённых итераторов и ссылок. Если освобождение памяти критично и должно быть обеспечено логикой приложения, применяют явную пересборку контейнера и учитывают её стоимость.
Можно ли после shrink_to_fit гарантированно использовать старый итератор, если size() не изменился?
Нет. Сохранение размера не означает сохранение адресов элементов. Если реализация уменьшила буфер через перераспределение, старый итератор недействителен. Использование его после вызова без доказанного отсутствия перераспределения — неопределённое поведение.
Обязан ли shrink_to_fit освободить весь запас у пустого вектора?
Нет. Даже для пустого контейнера это необязательный запрос. capacity() может уменьшиться, но стандарт не требует, чтобы она стала нулевой или чтобы выделенная память была возвращена распределителю.
Гарантирует ли повторный вызов shrink_to_fit, что память в итоге будет освобождена?
Нет. Каждый вызов остаётся необязательным запросом, поэтому повторение не превращает его в гарантию. Кроме того, уменьшение capacity() контейнера не тождественно немедленному возврату памяти операционной системе: поведение распределителя памяти находится за пределами гарантий std::vector.