unique_ptr: единоличное владение

Вопрос с собеседования: почему этот код не компилируется?

$$ $$

#include <memory>
#include <iostream>

void takeOwnership(std::unique_ptr<int> p) {
    std::cout << "Владею числом: " << *p << "\n";
}

int main() {
    std::unique_ptr<int> a = std::make_unique<int>(42);
    std::unique_ptr<int> b = a; // ошибка компиляции
    takeOwnership(a);           // тоже ошибка
}

Вывод:

error: use of deleted function
'std::unique_ptr<int>::unique_ptr(const std::unique_ptr<int>&)'

Компилятор прямым текстом говорит: конструктор копирования у unique_ptr удалён. И это не баг, а осознанное решение авторов стандартной библиотеки, которое стоит понимать буквально с первого слова названия — unique, «единственный».

unique_ptr — это тонкая RAII-обёртка вокруг сырого указателя. Внутри у него буквально лежит один указатель на объект в куче (heap), и когда unique_ptr выходит из области видимости — например, функция заканчивается, — в его деструкторе автоматически вызывается delete для этого указателя. Ты пишешь new один раз при создании, а delete не пишешь вообще — за тебя это сделает деструктор unique_ptr, по той же схеме RAII, что мы разбирали для Logger в прошлом уроке.

Ключевое правило: у ресурса, которым владеет unique_ptr, в любой момент времени может быть ровно один владелец. Если бы копирование было разрешено, у нас получилось бы два unique_ptr, указывающих на один и тот же объект в куче. Оба они, умирая, попытались бы вызвать delete на одном и том же адресе — это называется double free (двойное освобождение), одна из самых опасных ошибок в C++: она портит память кучи и часто приводит к падению программы в совершенно другом, не связанном месте кода. Чтобы такая ошибка была невозможна физически, а не «по соглашению между программистами», авторы языка просто удалили у unique_ptr конструктор и оператор копирования — компилятор не даст скомпилировать код, где происходит копия.

Но как тогда передать владение указателем в другую функцию или переменную? Ответ — перемещение (move), а не копирование. У unique_ptr есть move-конструктор, который «ворует» указатель у исходного объекта, а исходный обнуляет (переводит в состояние nullptr). Владелец остался один — просто сменился.

$$ $$

std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = std::move(a); // владение переехало в b

std::cout << (a == nullptr) << "\n"; // 1 — a теперь пуст
std::cout << *b << "\n";              // 42 — b владеет числом

Вывод:

1
42

std::move здесь не двигает никакие байты физически — это просто приведение типа, которое говорит компилятору «можно забрать содержимое этого объекта, он мне больше не нужен в исходном виде». После move исходный unique_ptr a гарантированно становится пустым (nullptr), и попытка разыменовать его — это неопределённое поведение (undefined behavior), которое на собеседовании тоже любят спрашивать отдельным вопросом.

Та же логика работает и с передачей в функцию по значению, как в примере с ошибкой компиляции выше: если функция принимает unique_ptr<int> по значению, вызвать её можно только через takeOwnership(std::move(a)) — и после такого вызова переменная a в вызывающем коде опустеет, потому что владение реально переехало внутрь функции.

Почему в реальных проектах unique_ptr — предпочтительный указатель «по умолчанию»? Потому что у него нулевой накладной расход по сравнению с сырым указателем (никакого счётчика ссылок, никакой дополнительной памяти — размер тот же, что у обычного указателя) и при этом полная гарантия от утечек памяти и double free. Правило хорошего тона в современном C++: используй unique_ptr, пока не докажешь себе, что тебе реально нужно разделяемое владение — и вот тогда переходи к shared_ptr, о котором в следующем уроке.

Проверьте себя
1. Почему у std::unique_ptr удалён конструктор копирования?
AЧтобы сэкономить память — копия хранила бы дублирующие данные
BЧтобы не допустить двух владельцев одного ресурса и связанного с этим double free
CПотому что компилятор не умеет копировать указатели
DЭто временное ограничение, которое уберут в будущих стандартах
2. Что происходит с исходным unique_ptr после std::move(a)?
AОн остаётся владельцем наравне с новым указателем
BОн становится пустым (nullptr), владение полностью перешло новому указателю
CПрограмма падает с ошибкой во время выполнения
DОн автоматически создаёт копию объекта для себя