Разберите последствия перемещения std::vector: что можно гарантировать о состоянии исходного контейнера после операции?
После перемещения исходный std::vector остаётся валидным, но его состояние не определено точно: стандарт гарантирует лишь, что с ним можно безопасно выполнять разрешённые операции, например уничтожить его, присвоить ему новое значение или вызвать clear(). Нельзя полагаться на то, что он пуст, хотя на практике обычный вектор чаще всего передаёт буфер и становится пустым.
Содержимое и размер целевого контейнера после успешного перемещения соответствуют исходному контейнеру до операции. Проверять конкретный size() исходного вектора без дополнительной договорённости нельзя.
Перемещающие операции появились в C++11, чтобы передавать владение динамическими ресурсами без дорогостоящего копирования. Для std::vector это особенно важно: копирование требует создать новый буфер и переместить или скопировать все элементы, а обычное перемещение может передать указатель на уже существующий буфер.
Такой подход используется при возврате контейнеров из функций, передаче временных объектов и изменении владельца больших последовательностей. При этом язык сознательно не закрепляет конкретное состояние объекта-источника после перемещения.
Ошибка возникает, когда после перемещения разработчик продолжает использовать исходный вектор так, будто он обязательно пуст:
Безопасно использовать исходный объект в соответствии с его API, но нельзя извлекать из его состояния бизнес-смысл или ожидать сохранения конкретных элементов. Нельзя также обращаться к элементам через старые итераторы и ссылки, не учитывая правила конкретной операции перемещения.
std::move сам по себе ничего не перемещает. Это преобразование выражения к rvalue-ссылке, позволяющее выбрать перемещающий конструктор или перемещающий оператор присваивания.
При обычном перемещающем конструкторе std::vector реализация обычно передаёт целевому объекту указатель на буфер, размер и вместимость исходного контейнера. Это позволяет выполнить операцию за константное время независимо от числа элементов. Исходный объект затем должен сохранить внутреннее согласованное состояние, но стандарт не требует, чтобы оно было именно пустым.
После перемещения гарантируется следующее:
size(), capacity() и содержимое исходного контейнера не следует считать гарантированными.У перемещающего присваивания есть дополнительная тонкость: поведение зависит от аллокатора. Если аллокаторы можно передать вместе с буфером или они совместимы, контейнер обычно забирает существующее хранилище. При несовместимых аллокаторах может потребоваться поэлементное перемещение, поэтому операция способна стать линейной.
Ссылки, указатели и итераторы требуют отдельного анализа. Для обычного перемещающего конструктора контейнеров стандарт гарантирует сохранение действительности ссылок, указателей и итераторов на элементы исходного контейнера, кроме итератора end(); они начинают обозначать элементы нового контейнера. Для перемещающего присваивания и вариантов с явно переданным аллокатором такие гарантии могут отличаться, поэтому переносить это правило автоматически нельзя.
Сервис формирует большой пакет данных и передаёт его в очередь. Рассматривались три варианта: копирование в очередь, передача временного объекта и перемещение именованного вектора.
Копирование надёжно сохраняет исходный вектор, но требует дополнительной памяти и времени, пропорциональных числу элементов. Передача временного объекта обычно позволяет компилятору выбрать перемещение, однако подходит не для всех интерфейсов. Перемещение именованного вектора устраняет копирование буфера, но после него исходный объект нельзя использовать как источник данных.
Выбран вариант с явным перемещением, поскольку исходный вектор после постановки в очередь больше не нужен. Код не проверяет, стал ли он пустым, а при необходимости повторного использования явно присваивает ему новое значение. Это сохраняет корректность независимо от конкретной реализации контейнера и политики аллокатора.
std::vector объект пуст?Нет. Гарантируется валидность объекта, но не конкретное содержимое или размер. Проверка empty() допустима как получение текущего состояния, однако результат нельзя заранее предсказать и нельзя использовать как универсальный контракт.
Для обычного перемещающего конструктора ссылки, указатели и итераторы на элементы, кроме итератора end(), сохраняют действительность и начинают обозначать соответствующие элементы нового контейнера. Это не означает, что аналогичная гарантия автоматически действует для перемещающего присваивания или перемещения с явно заданным несовместимым аллокатором.
std::move отсутствие перемещения каждого элемента?Нет. std::move только меняет категорию выражения и помогает выбрать перегрузку, принимающую rvalue. Сам контейнер может передать буфер целиком, но при ограничениях аллокатора или в другой операции может выполнять поэлементное перемещение, которое имеет линейную сложность и может вызывать перемещающие конструкторы элементов.