Разберите, почему циклическое владение через std::shared_ptr может продлить жизнь объектов навсегда.
Циклическое владение через std::shared_ptr не позволяет счётчикам сильных владельцев обнулиться, поэтому объекты могут остаться в памяти после исчезновения всех внешних ссылок. std::weak_ptr разрывает такой цикл: он наблюдает за объектом, но не увеличивает число его владельцев.
Умные указатели появились как средство автоматизировать управление временем жизни динамических объектов и уменьшить количество ручных операций delete. std::shared_ptr решает задачу совместного владения ресурсом, когда заранее нельзя назначить одного владельца.
Однако подсчёт ссылок не является сборкой мусора. Он освобождает объект только тогда, когда число сильных владельцев становится равным нулю, поэтому циклические зависимости требуют отдельного механизма наблюдения — std::weak_ptr.
Представим два объекта, каждый из которых хранит сильную ссылку на другой. После удаления внешних shared_ptr первый объект всё ещё удерживается вторым, а второй — первым.
В результате деструкторы объектов не вызываются, занятая ими память и связанные ресурсы не освобождаются. Для долгоживущих графов объектов, кэшей или подписок это может привести к постепенной утечке памяти.
Каждая копия std::shared_ptr увеличивает счётчик сильных владельцев в общем управляющем блоке. Когда такой указатель уничтожается или переназначается, счётчик уменьшается; объект уничтожается при достижении нуля.
В цикле счётчик не достигает нуля сам по себе: внутренние ссылки участников цикла продолжают владеть объектами. Умный указатель не анализирует граф ссылок и не распознаёт, что эти владения больше недостижимы из внешней программы.
std::weak_ptr связан с тем же управляющим блоком, но не считается владельцем и не увеличивает сильный счётчик. При необходимости временного доступа вызывают lock(): если объект ещё жив, получается shared_ptr, иначе возвращается пустой указатель.
Здесь parent владеет child, а child только наблюдает за parent. После завершения main внешние владельцы уничтожаются, сильное владение child со стороны parent освобождает дочерний объект, а weak_ptr не удерживает родительский объект.
weak_ptr нельзя разыменовать напрямую: сначала нужно получить временный shared_ptr через lock(), а затем проверить его непустое состояние. Это важно, поскольку объект может быть уничтожен между моментом проверки и попыткой использования при неправильной синхронизации.
Сильное владение следует использовать только там, где объект действительно отвечает за продление времени жизни другого объекта. Для обратных ссылок, наблюдателей, родителей в деревьях, кэшированных объектов и регистраций обратного вызова обычно подходит weak_ptr, но выбор зависит от требуемой семантики времени жизни.
В графе объектов узел хранил shared_ptr на соседние узлы, а каждый сосед также хранил shared_ptr обратно. После удаления графа память не освобождалась: внутренние ссылки продолжали поддерживать циклы владения.
Рассматривались три варианта. Ручной вызов delete был отвергнут из-за риска двойного освобождения и висячих ссылок. Полная перестройка модели с единственным владельцем могла бы решить проблему, но усложнила доступ к узлам и изменение структуры графа. Замена обратных сильных ссылок на weak_ptr сохранила совместное владение там, где оно действительно нужно, и убрала цикл.
Выбрали третий вариант: исходящие связи, определяющие владение, оставили сильными, а обратные и навигационные связи сделали слабыми. В результате удаление внешнего корня стало освобождать недостижимые узлы, а временный доступ выполнялся через проверяемый lock().
Разрушается ли цикл, если все внешние shared_ptr уничтожены?
Нет. Уничтожение внешних указателей уменьшает счётчики, но внутренние сильные ссылки в цикле остаются. Пока хотя бы одна такая ссылка удерживает каждый объект цикла, соответствующие сильные счётчики не становятся нулевыми.
Почему нельзя заменить weak_ptr на обычный сырой указатель без изменения смысла?
Сырой указатель не выражает отношения наблюдения в интерфейсе и не предоставляет безопасного способа проверить, жив ли объект. Он может стать висячим сразу после уничтожения владельца. weak_ptr связан с управляющим блоком, поэтому через lock() можно атомарно получить сильное владение на время использования.
Всегда ли наличие shared_ptr означает утечку при наличии цикла?
Нет. Цикл становится проблемой, если после исчезновения внешних владельцев он остаётся недостижимым, но сохраняет ненулевое число сильных ссылок. Если цикл разрывается внешним действием или хотя бы одна связь не является сильной, объекты могут быть корректно уничтожены.
Кроме того, shared_ptr может удерживать не только память объекта, но и другие ресурсы, освобождаемые его удалителем. Поэтому цикл способен задержать закрытие файла, соединения или другого ресурса, даже если сама утечка памяти внешне невелика.