В API объект с автоматическим временем жизни передали в std::shared_ptr: к чему приведёт уничтожение последнего владельца?
Если std::shared_ptr создан из указателя на объект с автоматическим временем жизни, уничтожение последнего владельца приведёт к вызову стандартного удалителя, то есть к применению delete к этому указателю. Это неопределённое поведение: объект не был создан через обычный new, а его память не должна освобождаться таким способом.
RAII появился как способ связать управление ресурсом с временем жизни объекта: ресурс захватывается при создании объекта и освобождается его деструктором. Указатели сами по себе не выражали, кто владеет объектом и кто обязан его уничтожить.
std::shared_ptr решает задачу совместного владения через управляющий блок со счётчиком владельцев и удалителем. Он предполагает, что переданный указатель действительно можно уничтожить соответствующим удалителем; создание shared_ptr не превращает любой адрес в безопасно управляемый ресурс.
Объект с автоматическим временем жизни уничтожается автоматически при выходе из своей области видимости. Если передать его адрес в обычный std::shared_ptr, умный указатель ошибочно станет считать себя владельцем этого объекта.
При уничтожении последнего владельца shared_ptr попытается освободить объект. Для стандартного удалителя это означает вызов delete, что для локального объекта является неопределённым поведением. Возможны некорректный вызов деструктора, попытка освобождения стека и последующее повторное уничтожение объекта при выходе из области видимости.
Конструктор std::shared_ptr<T> из T* принимает владение указателем согласно стандартной модели удаления. Если пользовательский удалитель не задан, управляющий блок использует удаление, эквивалентное delete для одиночного объекта.
В этом примере при уничтожении owner shared_ptr попытается удалить object. Затем область функции всё равно завершится, и автоматический объект будет уничтожаться своим обычным способом. Уже первая попытка удалить объект делает программу некорректной; конкретное проявление предсказать нельзя.
Если объект не должен передаваться во владение, следует использовать сырой указатель или ссылку как невладеющее представление, гарантируя, что объект живёт дольше операции. Если требуется совместное владение, объект нужно создать сразу в модели совместного владения, например через std::make_shared.
Технически можно задать удалитель, который ничего не делает. Это предотвращает вызов delete, но не продлевает жизнь объекта и позволяет shared_ptr пережить локальный объект. После выхода объекта из области видимости такой shared_ptr станет висячим, поэтому это допустимо только в узком адаптационном сценарии с внешней гарантией времени жизни.
Библиотека принимает std::shared_ptr<Session> и сохраняет его для асинхронного вызова. Разработчик передал туда адрес локального Session, рассчитывая, что shared_ptr «безопасно разберётся» с временем жизни объекта.
Вариант с обычным конструктором shared_ptr приводит к неопределённому поведению при уничтожении последнего владельца. Вариант с удалителем без действия устраняет попытку освободить стек, но асинхронная операция может обратиться к объекту после выхода из функции. Вариант с сырым указателем сохраняет ту же проблему, если библиотека действительно хранит указатель.
Корректное решение — создать Session в динамической памяти под управлением std::make_shared, если библиотека должна разделять владение. Тогда объект будет жить до уничтожения последнего shared_ptr, а память и деструктор будут обработаны согласованно. Если библиотека лишь выполняет синхронный вызов и ничего не сохраняет, ей лучше принимать ссылку или невладеющий указатель с явно документированным ограничением времени жизни.
Может ли пользовательский удалитель сделать передачу локального объекта в shared_ptr полностью безопасной?
Удалитель без действия предотвращает неправильное освобождение памяти, но не меняет время жизни объекта. Если shared_ptr копируется или сохраняется, он может остаться после уничтожения локального объекта и содержать недействительный адрес. Поэтому такой подход не создаёт владение, а лишь маскирует часть ошибки.
Что произойдёт, если локальный объект является подобъектом другого объекта?
Передача адреса подобъекта в обычный shared_ptr также некорректна. delete должен применяться к указателю, полученному из совместимого выделения памяти, а подобъект не является отдельным объектом, выделенным через new; кроме того, его временем жизни управляет содержащий объект.
Достаточно ли того, что shared_ptr создан, чтобы сырой указатель на объект оставался действительным?
Да, пока существует хотя бы один корректный владеющий shared_ptr, созданный с правильным удалителем, объект обычно остаётся жив. Но один сырой указатель владения не содержит: после уничтожения последнего владельца он становится недействительным. Для временного доступа нужно удерживать копию shared_ptr на весь период использования либо использовать другой механизм синхронизации времени жизни.