Сравнение: чем отличается освобождение памяти при создании std::shared ptr через std::make shared от отдель...

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

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

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

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

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

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

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

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

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

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

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

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

В модели shared ownership есть два независимых условия:

  • объект жив, пока счётчик сильных владельцев не стал равен нулю;
  • управляющий блок жив, пока существуют сильные или слабые ссылки.

При std::make_shared<T> обычно выполняется одно выделение памяти для управляющего блока и объекта T. Когда последний std::shared_ptr уничтожен, вызывается деструктор T, но само выделение ещё нельзя освободить: оставшийся std::weak_ptr должен иметь доступ к счётчику и другим данным управляющего блока. Память освобождается после исчезновения последней сильной и слабой ссылки.

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

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

Минимальная иллюстрация различия:

#include <memory> struct Large { char data[1024 * 1024]; }; void example() { std::weak_ptr<Large> weak; { auto shared = std::make_shared<Large>(); weak = shared; } // Large уничтожен, но общая область может ещё сохраняться { auto raw = std::make_unique<Large>(); auto shared = std::shared_ptr<Large>(std::move(raw)); weak = shared; } // память Large освобождена отдельно от управляющего блока }

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

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

Кэш хранит слабые ссылки на крупные результаты вычислений, чтобы не препятствовать их удалению, когда ими больше никто не владеет. После перехода на std::make_shared выяснилось, что память управляющих областей долго не возвращается: кэш очищал записи не сразу, поэтому слабые ссылки продолжали существовать.

Вариант с сохранением make_shared прост и обычно быстрее по числу выделений, но может удерживать крупные области памяти. Полный отказ от std::weak_ptr решает проблему памяти, однако ломает назначение кэша и способен продлить жизнь объектов через сильные ссылки.

Выбранное решение — для крупных объектов использовать раздельное размещение объекта и управляющего блока, а для небольших объектов оставить make_shared. Это позволяет слабым ссылкам удерживать только небольшой управляющий блок, не задерживая освобождение памяти самого крупного объекта.

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

  1. Продлевает ли std::weak_ptr время жизни объекта?

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

  1. Всегда ли std::make_shared выполняет ровно одно выделение памяти?

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

  1. Почему пользовательский удалитель может быть причиной выбора раздельного создания?

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