JavaRush /Курсы /C++ SELF /std::unique_ptr

std::unique_ptrget() / reset() / release()

C++ SELF
43 уровень , 1 лекция
Открыта

1. Граница владения

Перед тем как нажимать на кнопки get(), reset() и release(), полезно остановиться на полсекунды и задать себе один взрослый вопрос: «кто будет владельцем ресурса после этой операции?».

В C++ ошибки владения редко выглядят как «красивое сообщение об ошибке» — чаще это либо утечки, либо внезапные падения, либо «вроде работает, но иногда» (что особенно страшно).

Представьте, что ресурс — это единственный ключ от квартиры. unique_ptr — человек, который этот ключ хранит и обязуется закрыть квартиру, когда уйдёт. Тогда get() — это «дать ключ посмотреть, но не отдавать», reset() — это «выкинуть ключ и закрыть квартиру (или поменять квартиру)», а release() — это «отдать ключ навсегда другому человеку и больше не отвечать за квартиру». И вот на последнем пункте люди обычно драматично ошибаются.

Нарисуем простую блок‑схему выбора операции:

flowchart TD
    A["Хочу что-то сделать с unique_ptr"] --> B{"Нужно ли сменить владельца?"}
    B -->|Нет, просто доступ| C["get() (наблюдение)"]
    B -->|Да| D{"Кто будет владеть после операции?"}
    D -->|Никто: ресурс должен исчезнуть или быть заменён| E["reset() (уничтожить/заменить)"]
    D -->|Кто-то другой, но не unique_ptr| F["release() (отдать наружу) + немедленный план владения"]

Главная мысль: get() не меняет владельца, reset() уничтожает (или заменяет) ресурс у текущего владельца, release() делает владельцем… кого-то снаружи, и это опасно.

2. get(): «дай посмотреть»

Когда вы впервые видите p.get(), рука может потянуться воспринимать это как «ну это же указатель, значит можно с ним делать всё». И вот тут C++ хитро улыбается.

get() действительно возвращает T*, но по смыслу это временный наблюдатель: доступ к объекту даётся, а ответственность за освобождение остаётся у unique_ptr.

Типичный сценарий get() — у вас есть функция, которая исторически принимает сырой указатель (или так удобнее выразить “nullable”), но не должна удалять объект. Тогда вы передаёте p.get(), и всё хорошо — пока вы не пытаетесь сделать из этого указателя второго владельца или хранить его «на потом».

Мини‑пример: у нас есть “сессия” приложения, которую мы печатаем. Печать не должна владеть сессией — она просто читает данные.

#include <iostream>
#include <memory>
#include <string>

struct Session {
    std::string user;
};

void print_session(const Session* s) {
    if (!s) { std::cout << "no session\n"; return; } // no session
    std::cout << "user = " << s->user << '\n';       // user = Alice
}

А теперь использование get():

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    print_session(sess.get());
}

Обратите внимание: print_session принимает const Session*, то есть «могу получить nullptr». Это хороший контракт для “наблюдения”. А sess остаётся владельцем.

Теперь важный анти‑пример, который компилируется, выглядит невинно, но логически преступен:

#include <memory>

int main() {
    auto a = std::make_unique<int>(5);
    std::unique_ptr<int> b(a.get()); // ОПАСНО: второй владелец!
}

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

Ещё одна “тихая” ловушка get() — хранить указатель дольше, чем гарантирована жизнь владельца. Если вы взяли T* raw = p.get(), а потом где-то раньше времени сделали p.reset(), то raw теперь смотрит в «уже освобождённую память». Указатель не станет автоматически nullptr, потому что он «не умный» (он просто адрес).

3. reset(): уничтожить или заменить ресурс

reset() — это операция, которая чаще всего ощущается как «навести порядок». Она нужна, когда владелец должен либо отпустить ресурс (стать пустым), либо заменить ресурс на другой.

И это звучит просто — но полезно держать в голове: reset() не просто меняет адрес внутри unique_ptr, он ещё и запускает уничтожение старого объекта (если он был).

В человеческом смысле reset() отвечает на вопрос: «я, как владелец, больше не хочу владеть этим объектом». И тогда объект уничтожается прямо сейчас, а не «когда-нибудь потом». Поэтому reset() — хороший инструмент, когда вы хотите заранее освободить ресурс (например, чтобы память освободилась до конца функции).

Мини‑пример: у нас есть опциональная сессия. По команде “logout” мы хотим удалить сессию.

#include <iostream>
#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset();                       // сессия уничтожена, sess пустой
    std::cout << std::boolalpha << !sess << '\n'; // true
}

Теперь вариант «замена ресурса». Представим, что “login” пересоздаёт сессию.

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess = std::make_unique<Session>(Session{"Bob"}); // старый Session уничтожен
}

Технически тут не написан reset(), но по смыслу происходит то же самое: старый ресурс освобождён, новый принят. Можно писать и так:

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset(new Session{"Bob"}); // рабочий вариант, но make_unique обычно лучше
}

Почему std::make_unique лучше — мы обсуждали раньше: он делает намерение понятнее и меньше провоцирует «ручные куски» управления.

Важно понять эффект: после reset() или замены, все “наблюдатели” (сырые T*, полученные через get() ранее) становятся потенциально опасными. Не потому что reset() плохой, а потому что вы поменяли/уничтожили объект, а наблюдатели об этом не знают.

4. release(): отдать владение наружу

release() — самая коварная из трёх операций. Она выглядит как «почти get()», но делает противоположное по смыслу.

Если get() даёт сырой указатель без передачи владения, то release() отдаёт сырой указатель с передачей владения: unique_ptr перестаёт быть владельцем и становится пустым (nullptr).

То есть после release() ответственность за delete возвращается… к вам. Да, к тому самому “вам”, который пришёл в unique_ptr именно потому, что не хотел думать про delete.

Поэтому правило простое: release() используем только тогда, когда вы точно знаете, кто станет следующим владельцем, и обычно — немедленно.

Мини‑пример “показать механику”:

#include <iostream>
#include <memory>

int main() {
    auto p = std::make_unique<int>(10);

    int* raw = p.release();                 // p пустой
    std::cout << (p == nullptr) << '\n';    // 1

    delete raw; // теперь мы обязаны удалить сами (иначе утечка)
}

В реальном “современном” коде так делать не хочется. Более безопасный сценарий — вы сделали release() только чтобы тут же передать владение другому владельцу (например, если у вас есть API, принимающий T* и обещающий “теперь я владелец”).

Например, мы можем сделать функцию “принять владение и завернуть обратно в unique_ptr”, чтобы на границе “старого/нового мира” не держать сырые указатели долго:

#include <memory>

std::unique_ptr<int> adopt(int* p) {
    return std::unique_ptr<int>(p);
}

int main() {
    auto a = std::make_unique<int>(7);
    auto b = adopt(a.release()); // владение переехало, a пустой
}

Смысл правильный: сырой указатель существует очень коротко и сразу снова оказывается под RAII‑владельцем.

Ещё раз: release() — это не “получить указатель”. Это “отдать владение”.

Шпаргалка: get() vs reset() vs release()

Когда голова устала, таблица спасает. Здесь важно не «как выглядит», а «что становится с владельцем» и «кто отвечает за удаление».

Операция Что возвращает Меняет ли владельца? Состояние unique_ptr после Кто удаляет объект
get()
T*
Нет Не меняется Всё ещё unique_ptr
reset()
ничего Да (владелец меняет/удаляет ресурс) Обычно
nullptr
(или новый ресурс)
unique_ptr удаляет старый ресурс сам
release()
T*
Да (владение уходит наружу) Становится
nullptr
Тот, кто получил
T*
(вручную или новый владелец)

Эта логика хорошо укладывается в «правило одного владельца»: get() владельца не добавляет, reset() владельца не добавляет (владелец тот же, просто ресурс меняется), а вот release() буквально говорит: «теперь владельца внутри unique_ptr нет».

5. Практический пример: ToDo‑сессия и границы владения

Чтобы тема не оставалась чисто теоретической, давайте продолжим собирать наше учебное консольное приложение (маленький ToDo/заметки). Мы не уходим в ООП, поэтому сделаем простые struct и функции.

Начнём с данных: задачи и сессия.

#include <string>
#include <vector>

struct Task {
    std::string text;
    bool done{};
};

struct Session {
    std::string user;
    std::vector<Task> tasks;
};

Теперь сделаем две функции “наблюдателя”: одна печатает сессию, другая печатает количество задач. Они принимают const Session*, потому что сессии может не быть (например, пользователь не вошёл).

#include <iostream>

void print_user(const Session* s) {
    if (!s) { std::cout << "guest\n"; return; }     // guest
    std::cout << s->user << '\n';                   // Alice
}

void print_task_count(const Session* s) {
    if (!s) { std::cout << 0 << '\n'; return; }     // 0
    std::cout << s->tasks.size() << '\n';           // 2
}

А теперь в main создадим unique_ptr<Session> и будем безопасно отдавать “посмотреть” через get():

#include <memory>

int main() {
    std::unique_ptr<Session> sess; // пусто: не залогинен

    print_user(sess.get());

    sess = std::make_unique<Session>(Session{"Alice", {{"Buy milk", false}}});
    print_user(sess.get());
}

Здесь get() — идеальный инструмент: мы не передаём владение, мы просто даём доступ для чтения.

Теперь добавим “logout”, где естественно использовать reset():

#include <iostream>
#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    sess.reset();                                 // удалили сессию
    std::cout << std::boolalpha << !sess << '\n'; // true
}

Смысл простой: владелец тот же (sess), но он стал пустым, а объект уничтожен.

И наконец — ситуация, когда кто-то “снаружи” хочет забрать владение. В нормальном современном дизайне лучше бы принимать unique_ptr напрямую, но допустим, у нас есть условно “legacy API”:

#include <iostream>

void legacy_take_session(Session* s) {
    // Представим, что legacy-система обещает: "я удалю s сама"
    std::cout << "legacy got session\n"; // legacy got session
    delete s;
}

Тогда граница выглядит так:

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    legacy_take_session(sess.release()); // отдали владение наружу
    // sess теперь пустой, и это нормально
}

Здесь release() оправдан: вы действительно отдаёте владение наружу, и следующий владелец явно определён (legacy‑функция). Но если legacy‑функция не удалит объект — будет утечка. Если она удалит, а вы потом попробуете удалить ещё раз — будет double‑free. Поэтому release() требует дисциплины и ясного контракта.

6. Типичные ошибки

Ошибка №1: делать второго владельца из get().
Это одна из самых неприятных логических ошибок, потому что выглядит “естественно”: вы получили T* и завернули его в новый unique_ptr. Проблема в том, что первый unique_ptr никуда не делся и продолжает считать себя владельцем. В результате два владельца попытаются освободить один ресурс. Правильная мысль такая: get() — только посмотреть, владельца не создаём.

Ошибка №2: использовать сырой указатель после reset() или замены ресурса.
Часто это происходит случайно: вы сохранили Task* t = ...get(), потом где-то выше по коду сделали reset() (или просто присвоили новый unique_ptr), и указатель внезапно стал висячим. unique_ptr здесь не виноват: он честно уничтожил объект. Виноваты мы, потому что забыли, что наблюдатель не продлевает жизнь владельца.

Ошибка №3: воспринимать release() как “просто получить T*”.
release() — это “я больше не владею, забирайте и отвечайте сами”. Если после release() вы не передали указатель новому владельцу и не удалили его вручную, утечка почти гарантирована. Поэтому release() используют редко и обычно “на границе” со старым API, где владение выражается сырьём.

Ошибка №4: думать, что после release() unique_ptr всё ещё можно разыменовывать.
После release() unique_ptr становится пустым (nullptr). Он валиден как объект (его можно присвоить заново), но разыменовывать *p или p->field уже нельзя без проверки. Это та же дисциплина, что и после std::move: источник чаще всего становится пустым и должен считаться пустым, пока вы не доказали обратное.

Ошибка №5: смешивать “кто владеет” и “кто пользуется” в одной функции без контракта.
Например, функция принимает Session* и где-то внутри иногда делает delete, а иногда нет. Снаружи невозможно понять, кто отвечает за ресурс. В итоге один вызов даёт утечку, другой — double‑free. Даже если вы вынуждены работать с сырыми указателями, держите чёткое правило: либо функция всегда владеет и всегда удаляет, либо никогда не удаляет (и это должно быть видно из дизайна/названия/документации). unique_ptr как раз помогает сделать такие границы очевиднее.

1
Задача
C++ SELF, 43 уровень, 1 лекция
Недоступна
Наблюдатель датчика
Наблюдатель датчика
1
Задача
C++ SELF, 43 уровень, 1 лекция
Недоступна
Вход и выход
Вход и выход
1
Задача
C++ SELF, 43 уровень, 1 лекция
Недоступна
Живые токены
Живые токены
1
Задача
C++ SELF, 43 уровень, 1 лекция
Недоступна
Старый модуль
Старый модуль
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ