Какой статус имеет объект после успешного перемещения в современном C++ и какие операции над ним остаются корректными?
После перемещения объект остаётся валидным, но обычно находится в состоянии, не определённом для конкретного значения. Над ним безопасно выполнять операции, разрешённые для валидного объекта, прежде всего уничтожение, присваивание нового значения и явно документированные операции; нельзя полагаться на прежнее содержимое.
Важно: std::move сам ничего не перемещает. Он лишь преобразует выражение к значению, которое может быть использовано перегрузкой перемещения.
В C++11 появились rvalue-ссылки и семантика перемещения. Они решают проблему лишних глубоких копирований временных объектов и объектов, срок жизни которых завершается.
До этого передача ресурсоёмкого объекта часто требовала копирования: выделения памяти, дублирования буфера или увеличения счётчиков владения. Перемещающий конструктор может забрать внутренний ресурс, например указатель на буфер, вместо его копирования.
После передачи объекта в операцию перемещения разработчик иногда продолжает использовать его так, будто содержимое осталось прежним. Это приводит к неверной логике: журнал может вывести пустую строку, контейнер — оказаться пустым, а пользовательский тип — сохранить только часть прежнего состояния.
Другая ошибка — считать перемещённый объект полностью непригодным и не учитывать, что он обязан оставаться валидным. Такой объект должен корректно уничтожаться и обычно может получить новое значение через присваивание.
Состояние перемещённого объекта определяется как валидное, но не специфицированное, если конкретный тип не устанавливает более строгую гарантию. «Не специфицированное» означает, что стандарт не обещает конкретное значение, но объект должен сохранять инварианты типа и поддерживать разрешённые операции.
Типичный безопасный сценарий — уничтожить объект, присвоить ему новое значение или вызвать операцию, для которой документация допускает произвольное валидное состояние. Нельзя без дополнительной гарантии проверять старое содержимое или строить на нём бизнес-логику.
В примере std::move разрешает выбрать перемещающий конструктор строки, но не задаёт результат для source. Вызовы clear и присваивание корректны, поскольку строка остаётся валидным объектом.
Перемещающий конструктор или оператор присваивания должны сохранять инварианты исходного типа. Например, объект-владелец ресурса после перемещения обычно не должен освобождать ресурс, переданный новому объекту. Конкретные гарантии зависят от типа и его документации: у некоторых типов состояние после перемещения описано точнее, чем общая гарантия стандарта.
Следует отличать перемещение объекта от перемещения его отдельных членов. Пользовательский перемещающий конструктор может оставить часть полей без изменения, сбросить их или переместить; это должно соответствовать инвариантам класса и его контракту. Поэтому полагаться на одинаковое поведение всех типов нельзя.
Также важно, что перемещение не всегда дешевле копирования и не обязано физически переносить ресурс. Для небольших типов копирование может быть оптимизировано не хуже, а перемещающая операция может быть недоступна или выбрана только при наличии подходящей перегрузки.
В сервисе объект запроса содержит большой буфер и передаётся в очередь задач. После передачи разработчик записывал размер буфера исходного объекта в журнал, предполагая, что буфер сохранился. Для некоторых реализаций размер становился нулевым, а для других оставался ненулевым, что делало диагностику непредсказуемой.
Рассматривались два варианта. Первый — копировать запрос перед постановкой в очередь: это сохраняло исходное состояние, но увеличивало потребление памяти и время обработки. Второй — перемещать запрос и не использовать исходный объект как источник данных после передачи: это устраняло лишнее копирование, но требовало изменить порядок журналирования.
Выбран второй вариант: нужные метаданные записывались до перемещения, а после него исходный объект сразу считался объектом для уничтожения или повторного присваивания. В результате сохранилась производительность, а код перестал зависеть от конкретного состояния перемещённого буфера.
Да, если этот метод допускает состояние объекта после перемещения и не требует конкретного значения его данных. Уничтожение, присваивание и многие операции восстановления состояния обычно корректны. Однако вызов метода, который предполагает наличие прежнего ресурса или определённого содержимого, допустим только при явной гарантии типа.
Нет. Пустое состояние часто удобно реализовать, но общая гарантия C++ этого не требует. Например, конкретный тип может оставить часть данных без изменений, перенести ресурс и сохранить служебное состояние; проверять результат можно только по документации типа, а не по общему правилу о перемещении.
std::move — это условное преобразование типа выражения к rvalue-ссылке. Оно сообщает перегрузке функции, что ресурс можно рассматривать как предназначенный для передачи, но фактическое изменение объекта выполняет выбранный перемещающий конструктор или оператор присваивания.
Если подходящей перемещающей операции нет, может быть выбрана копирующая операция, а в некоторых случаях выражение вообще не скомпилируется. Поэтому наличие std::move не является гарантией ни фактического перемещения, ни обнуления исходного объекта.