Функция возвращает указатель на объект, которым владеет локальный RAII объект. Разберите последствия вызова...

Функция возвращает указатель на объект, которым владеет локальный RAII-объект. Разберите последствия вызова get():

#include <iostream>
#include <memory>

int* get() {
    auto value = std::make_unique<int>(42);
    return value.get();
}

int main() {
    int* p = get();
    std::cout << *p << '
';
}
Проходите собеседования с ИИ помощником Hintsage

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

После выхода из get() объект std::unique_ptr<int> value уничтожается, а вместе с ним освобождается int. Указатель p становится висячим, поэтому выражение *p вызывает неопределённое поведение.

value.get() возвращает только невладеющий адрес; он не передаёт владение вызывающему коду. Для передачи владения функция должна вернуть std::unique_ptr<int>.

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

RAII возник как способ связать управление ресурсом с временем жизни объекта. Это решает проблему ручных парных операций вроде allocate/free или lock/unlock, когда ранний return, исключение или забытая ветка могут нарушить освобождение ресурса.

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

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

В функции get() объект value владеет динамически выделенным int. Вызов value.get() лишь извлекает адрес ресурса, но не изменяет владельца и не продлевает время жизни объекта.

При возврате из функции локальный unique_ptr уничтожается. Он освобождает int, поэтому p указывает на уже завершившийся объект. Разыменование такого указателя — неопределённое поведение: программа может вывести старое значение, аварийно завершиться или повредить состояние процесса.

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

std::unique_ptr реализует единоличное владение. При уничтожении владельца вызывается удаление управляемого объекта; после этого все ранее полученные через get() адреса становятся недействительными.

Исправленный вариант передаёт владение явно:

#include <iostream> #include <memory> std::unique_ptr<int> get() { return std::make_unique<int>(42); } int main() { auto p = get(); std::cout << *p << ' '; }

Здесь объект unique_ptr перемещается или возвращается с использованием оптимизации возврата, а время жизни int продолжается до уничтожения p. Возвращать std::unique_ptr следует, когда вызывающая сторона должна стать владельцем ресурса.

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

Нельзя исправлять исходный код вызовом delete p: память уже освобождена владельцем, поэтому повторное освобождение также приводит к неопределённому поведению. std::shared_ptr нужен только при действительно совместном владении; замена им unique_ptr без необходимости добавляет счётчик ссылок и усложняет модель владения.

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

В модуле конфигурации функция возвращала const char* на строку, созданную внутри локального std::string. После возврата некоторые запросы читали освобождённую или уже недействительную память, причём ошибка проявлялась только при другой оптимизации сборки.

Рассматривались варианты:

  • вернуть std::string — безопасно передаёт результат, но может копировать или перемещать строку;
  • вернуть std::unique_ptr<std::string> — явно передаёт владение, но избыточно для обычного значения;
  • вернуть std::string_view — эффективно, однако требует гарантировать время жизни исходной строки;
  • хранить строку в объекте конфигурации и возвращать ссылку — эффективно, но ссылка действительна только пока живёт конфигурация и не изменяет соответствующий объект.

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

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

  1. Можно ли вернуть value.get(), если перед этим вызвать value.release()?

    Да, адрес после release() не становится автоматически недействительным: unique_ptr перестаёт владеть объектом и возвращает сырой указатель. Но ответственность за последующее освобождение полностью переходит вызывающему коду.

    Например, результат можно принять в другой std::unique_ptr<int>:

    std::unique_ptr<int> get() { auto value = std::make_unique<int>(42); return std::unique_ptr<int>(value.release()); }

    На практике предпочтительнее сразу вернуть value, потому что это проще для чтения и безопаснее при сопровождении. После release() легко потерять ресурс или передать его неподходящему удалителю.

  2. Продлевает ли std::move(value) время жизни объекта после выхода из функции?

    Нет. std::move только превращает выражение в объект, из которого разрешено перемещение; сам по себе он не перемещает ресурс и не продлевает время жизни локального объекта.

    При return std::move(value) перемещается владение в возвращаемый unique_ptr, если операция действительно выполнена. Но явный std::move при возврате локального объекта часто не нужен и может помешать оптимизации возврата; корректный обычный вариант — return value;.

  3. Когда сырой указатель из unique_ptr::get() допустим?

    Он допустим как временное невладеющее представление, например при передаче в функцию, которая не сохраняет указатель и не удаляет объект. Владелец должен оставаться живым, а сам unique_ptr не должен за это время перемещаться, сбрасываться или уничтожаться.

    Такой указатель нельзя возвращать наружу без явного контракта времени жизни, сохранять дольше владельца или передавать функции, которая ожидает владение. Если владение нужно передать, используют перемещение unique_ptr, а если оно должно быть разделено — создают shared_ptr и передают его копию.