В контейнере хранится набор объектов с единственным владельцем. Определите, что произойдёт с владением ресу...

В контейнере хранится набор объектов с единственным владельцем. Определите, что произойдёт с владением ресурсами после перемещения контейнера:

#include <memory>
#include <vector>

int main() {
    std::vector<std::unique_ptr<int>> source;
    source.push_back(std::make_unique<int>(42));

    auto target = std::move(source);
}

Кому принадлежат объекты после этой операции и что гарантируется о состоянии source?

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

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

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

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

Ручное хранение владельцев в контейнерах требовало явно писать циклы удаления, копирования и обработки исключений. Подход RAII и std::unique_ptr позволил связать владение ресурсом со временем жизни объекта-владельца и сделать контейнер безопасным при обычном выходе из области видимости и при исключениях.

Поддержка перемещения в C++11 решила отдельную проблему: объект, владеющий уникальным ресурсом, нельзя копировать, но его можно эффективно передать другому объекту без дублирования ресурса.

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

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

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

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

Выражение std::move(source) само по себе ничего не перемещает: оно лишь преобразует source к ссылке, допускающей перемещение. Затем конструктор target перемещает внутреннее состояние вектора. Для стандартного std::allocator это обычно означает передачу буфера целиком, без перемещения каждого std::unique_ptr по отдельности.

После операции target владеет int со значением 42. source находится в корректном, но неопределённом состоянии: его можно уничтожить, присвоить ему новое значение, а для операций, допускаемых контрактом типа, нельзя предполагать конкретный размер или состав. В распространённой реализации source.empty() возвращает true, но стандартная гарантия должна рассматриваться осторожнее.

#include <memory> #include <vector> int main() { std::vector<std::unique_ptr<int>> source; source.push_back(std::make_unique<int>(42)); std::vector<std::unique_ptr<int>> target = std::move(source); // target владеет объектом; source больше не следует считать его владельцем source = {}; target.clear(); // освобождает int ровно один раз }

Деструктор target уничтожит свои std::unique_ptr, а те вызовут delete для принадлежащих объектов. Деструктор source не должен освобождать эти объекты повторно: после корректного перемещения уникальное владение не дублируется.

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

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

Сервис загружает несколько независимых обработчиков и хранит их в std::vector<std::unique_ptr<Handler>>. При передаче набора обработчиков в другой компонент возможны три решения.

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

Перемещение std::vector<std::unique_ptr<Handler>> явно передаёт единственное владение целиком. Это выбранное решение: оно не копирует обработчики, не создаёт общий контрольный блок и автоматически освобождает ресурсы при уничтожении нового владельца.

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

  1. Можно ли обращаться к элементам source после перемещения?

Нельзя использовать элементы source с предположением, что они остались на месте. После перемещения сам контейнер валиден, но его содержимое не определено контрактом операции. Разрешены уничтожение, присваивание и другие операции, для которых тип гарантирует работу с объектом в перемещённом состоянии; практический код не должен строиться на том, что source обязательно пуст.

  1. Почему копирование такого контейнера не компилируется?

Копирующая операция вектора должна была бы скопировать каждый std::unique_ptr. У std::unique_ptr копирующий конструктор удалён, поскольку копирование нарушило бы единственность владения. Поэтому копирующий конструктор вектора с такими элементами также недоступен, хотя сам вектор можно переместить.

  1. Что изменится, если контейнер перемещают в уже непустой контейнер?

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