Что происходит с объектом, на который ссылается std::weak ptr, после уничтожения последнего std::shared ptr?

Что происходит с объектом, на который ссылается std::weak_ptr, после уничтожения последнего std::shared_ptr?

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

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

std::weak_ptr не владеет объектом, поэтому не продлевает его время жизни. После уничтожения последнего std::shared_ptr объект уничтожается, а std::weak_ptr становится просроченным: его expired() возвращает true, а lock() не позволяет получить новый std::shared_ptr.

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

std::weak_ptr появился как дополнение к модели совместного владения через std::shared_ptr. shared_ptr решает задачу автоматического освобождения объекта при исчезновении последнего владельца, но иногда ссылаться на объект нужно без продления его жизни.

Такой невладеющий доступ требуется, например, для связей между объектами, кэшей и наблюдателей. Отдельно weak_ptr позволяет разрывать циклы совместного владения, хотя сам по себе цикл в вопросе не рассматривается.

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

Если хранить в наблюдателе std::shared_ptr, наблюдатель станет владельцем объекта. Объект может остаться жив, пока существует этот наблюдатель, даже если основной владелец уже исчез.

std::weak_ptr устраняет это продление времени жизни, но требует проверки результата доступа. Между проверкой состояния и использованием объекта нельзя полагаться на отдельные вызовы expired() и lock(): объект может быть уничтожен другим потоком между ними.

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

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

Когда уничтожается последний shared_ptr, счётчик владельцев становится равен нулю. Для обычного shared_ptr вызывается delete над объектом, поэтому заканчивается его время жизни. Сам контрольный блок может сохраниться, пока существует хотя бы один weak_ptr.

weak_ptr::expired() сообщает, что владельцев больше нет. Для безопасного доступа обычно используют lock(): если объект ещё жив, метод возвращает новый shared_ptr, временно увеличивая число владельцев; если объект уже уничтожен, возвращается пустой shared_ptr.

#include <memory> #include <iostream> struct Item {}; int main() { std::weak_ptr<Item> observer; { auto owner = std::make_shared<Item>(); observer = owner; std::cout << (observer.expired() ? "expired" : "alive") << '\ '; } auto access = observer.lock(); std::cout << (access ? "alive" : "expired") << '\ '; }

Внутри первой области объект жив, потому что существует owner. После выхода из неё последний владелец уничтожается, и lock() возвращает пустой указатель. Сам observer при этом остаётся корректным объектом, но больше не даёт доступа к уничтоженному Item.

В многопоточном коде результат lock() нужно сохранить в локальный shared_ptr и пользоваться именно им. Этот локальный владелец гарантирует, что объект не будет уничтожен до окончания операции с ним.

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

В системе есть кэш объектов, а задачи очереди должны обращаться к объекту, только если он ещё существует. Рассматривались три варианта: хранить shared_ptr в каждой задаче, хранить сырой указатель или хранить weak_ptr.

shared_ptr делает задачу дополнительным владельцем и может удерживать объект дольше ожидаемого. Сырой указатель не продлевает жизнь, но после удаления объекта превращается в потенциально висячий указатель, а проверить его безопасность стандартными средствами нельзя.

Выбран weak_ptr. Перед выполнением задачи вызывается lock(), и при успешном результате задача работает с полученным shared_ptr; при пустом результате она пропускается или помечается как устаревшая. Это сохраняет автоматическое управление памятью и явно обрабатывает исчезновение объекта.

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

1. Вопрос: Остаётся ли контрольный блок после уничтожения последнего shared_ptr?

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

2. Вопрос: Почему нельзя сначала вызвать expired(), а затем отдельно получить доступ к объекту?

Ответ: expired() только сообщает состояние в конкретный момент и не резервирует объект. В многопоточной программе другой поток может уничтожить последний shared_ptr сразу после проверки. Метод lock() объединяет проверку и получение владения в одну атомарную операцию для соответствующей модели потокобезопасности weak_ptr, поэтому использовать нужно результат lock().

3. Вопрос: Можно ли восстановить объект из просроченного weak_ptr?

Ответ: Нет. После уничтожения объекта weak_ptr сохраняет лишь состояние контрольного блока, но не объект и не способ его создать заново. lock() вернёт пустой shared_ptr; для восстановления объекта приложение должно отдельно хранить данные, фабрику или другой источник, из которого объект можно создать снова.