Сравните проверку доступности объекта через std::weak ptr::expired с попыткой получить std::shared ptr чере...

Сравните проверку доступности объекта через std::weak_ptr::expired() с попыткой получить std::shared_ptr через lock(): какой способ корректен при возможном уничтожении объекта другим потоком?

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

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

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

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

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

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

Отдельная проблема возникает в многопоточных программах: состояние объекта может измениться между двумя последовательными операциями. Поэтому проверка доступности и получение владения должны выполняться как единая операция, что и предоставляет lock().

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

Вызов expired() сообщает состояние только на момент проверки. Если он вернул false, другой поток всё ещё может уничтожить последний std::shared_ptr до того, как текущий поток получит собственный владеющий указатель.

В результате попытка создать std::shared_ptr из уже просроченного std::weak_ptr может завершиться исключением std::bad_weak_ptr. Ещё опаснее вариант, при котором после проверки выполняется обращение к объекту через другой невладеющий указатель: объект уже может быть уничтожен, что приводит к неопределённому поведению.

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

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

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

Минимальный пример корректного шаблона:

#include <iostream> #include <memory> void use(std::weak_ptr<int> observer) { if (auto owner = observer.lock()) std::cout << *owner; } int main() { auto owner = std::make_shared<int>(42); std::weak_ptr<int> observer = owner; use(observer); owner.reset(); use(observer); }

Вызов lock() не выбрасывает исключение из-за истёкшего объекта: результатом будет пустой указатель. В отличие от этого, конструктор std::shared_ptr из std::weak_ptr при отсутствии объекта выбрасывает std::bad_weak_ptr, поэтому для условного получения владения обычно выбирают именно lock().

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

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

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

Кэш хранит объекты через std::shared_ptr, а фоновые задачи получают на них std::weak_ptr. Задача должна обработать объект только в том случае, если он ещё нужен системе, но не должна сама удерживать его в кэше навсегда.

Вариант с expired() и последующим созданием владельца плох: между проверкой и созданием std::shared_ptr другой поток может удалить объект. Вариант с сырым указателем ещё хуже, поскольку не создаёт гарантии времени жизни.

Выбранное решение — вызвать lock() непосредственно перед обработкой и использовать возвращённый std::shared_ptr. Если он пуст, задача завершает работу; если непуст, объект гарантированно жив до конца области действия локального владельца. Это устраняет гонку за время жизни, не добавляя искусственного владения со стороны фоновой задачи.

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

  1. Почему нельзя считать expired() достаточной проверкой?

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

  1. Что произойдёт, если объект уничтожается одновременно с вызовом lock()?

Результат определяется атомарной операцией внутри lock(). Либо операция успеет увеличить число сильных владельцев и вернёт непустой std::shared_ptr, либо обнаружит отсутствие объекта и вернёт пустой указатель. Промежуточного состояния, в котором возвращённый std::shared_ptr указывает на уже уничтоженный объект, быть не должно.

  1. Гарантирует ли успешный lock() безопасное изменение объекта из нескольких потоков?

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