Программирование C++Управление памятьюC++ разработчик системного программного обеспечения

Нужно переместить владение из одного непустого std::unique ptr в другой уже владеющий ресурсом. Как изменит...

Нужно переместить владение из одного непустого std::unique_ptr в другой уже владеющий ресурсом. Как изменится судьба обоих ресурсов?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

При move-присваивании старый ресурс объекта-получателя сначала освобождается его deleter-ом, после чего получатель принимает ресурс источника. Исходный std::unique_ptr теряет владение и становится пустым; сам ресурс при этом не уничтожается из-за перемещения.

Исторический контекст

Владение динамическими ресурсами долгое время передавалось через сырые указатели, что не показывало намерение владельца и легко приводило к двойному освобождению или утечкам. std::unique_ptr, появившийся в C++11, сделал единоличное владение частью типа и использовал перемещение для явной передачи ответственности.

Такой подход заменяет неявный договор между функциями проверяемой операцией: копирование запрещено, а перемещение означает передачу владения.

Постановка проблемы

Если целевой std::unique_ptr уже владеет ресурсом, простая замена внутреннего указателя без предварительного освобождения привела бы к утечке старого ресурса. Если же после перемещения исходный указатель продолжал бы считаться владельцем, один ресурс оказался бы у двух владельцев и мог быть освобождён дважды.

Корректный результат должен сохранить единственного владельца: старый ресурс получателя уничтожается, ресурс источника переходит получателю, а источник больше не должен использоваться для доступа к этому ресурсу.

Подробное решение

Операция move-присваивания логически выполняет следующие действия:

  1. Освобождает ресурс, которым уже владел получатель, используя его deleter.
  2. Забирает указатель на ресурс из исходного std::unique_ptr.
  3. Передаёт или присваивает состояние deleter-а согласно правилам типа.
  4. Оставляет исходный std::unique_ptr пустым.

Перемещается не сам объект ресурса, а дескриптор владения: обычно указатель и, возможно, состояние пользовательского deleter-а. Поэтому адрес объекта, которым владел источник, не меняется.

#include <memory> int main() { auto source = std::make_unique<int>(10); auto target = std::make_unique<int>(20); target = std::move(source); }

После присваивания значение 20 уничтожено, target владеет объектом со значением 10, а source пуст. Обращаться к объекту через source нельзя, но сам перемещённый из него std::unique_ptr можно уничтожить, сбросить или заново присвоить ему другой ресурс.

Операция std::move сама по себе ничего не перемещает: она только позволяет выбрать перегрузку, принимающую rvalue-ссылку. Реальное изменение владения выполняет оператор перемещения std::unique_ptr.

Важна совместимость deleter-ов. Move-присваивание между std::unique_ptr обычно требует, чтобы deleter целевого типа поддерживал необходимое присваивание; пользовательский deleter может влиять также на noexcept и размер std::unique_ptr. Если требуется просто обменять два владения без уничтожения ресурса, используют swap, а не последовательное ручное освобождение.

Ситуация из практики

Компонент заменяет открытый файл новым. У него уже есть std::unique_ptr с пользовательским deleter-ом для старого файлового дескриптора, а функция открытия возвращает новый std::unique_ptr.

Ручной вызов reset с последующим извлечением сырого указателя возможен, но увеличивает промежуток, в котором легко потерять владение или допустить исключение. Использование std::shared_ptr неоправданно: совместное владение не требуется и добавляет контрольный блок со счётчиком ссылок.

Выбранное решение — move-присваивание нового std::unique_ptr существующему. Старый дескриптор закрывается автоматически, новый становится единственным владельцем, а локальный источник безопасно уничтожается в пустом состоянии. Это уменьшает количество ручных операций и сохраняет гарантию освобождения при выходе из области видимости.

Что кандидаты часто упускают

  1. Уничтожается ли ресурс источника непосредственно из-за move-присваивания?

    Нет. Move-присваивание передаёт владение, поэтому ресурс, которым владел источник, продолжает существовать и затем уничтожается при уничтожении нового владельца. Непосредственно во время присваивания обычно уничтожается только прежний ресурс получателя.

  2. Что произойдёт, если целевой std::unique_ptr был пустым?

    Освобождать нечего. Он просто примет ресурс источника, а источник станет пустым. Таким образом, отличие от присваивания в непустой объект состоит только в отсутствии уничтожения прежнего ресурса получателя.

  3. Можно ли после перемещения разыменовать исходный std::unique_ptr?

    Нет. После корректного перемещения исходный std::unique_ptr не владеет переданным ресурсом и обычно содержит nullptr; разыменование было бы ошибкой. Его разрешено проверять на пустоту, уничтожать, вызывать у него reset или присваивать ему новое владение.