Сравнение: когда для ресурса с одним владельцем следует выбрать std::unique_ptr вместо std::shared_ptr?
std::unique_ptr следует выбирать, когда ресурс имеет одного логического владельца, а передачу владения можно выразить явно. std::shared_ptr нужен только тогда, когда несколько независимых объектов действительно должны совместно продлевать время жизни одного ресурса.
std::unique_ptr обычно предпочтительнее благодаря отсутствию счётчика ссылок, меньшей сложности владения и невозможности случайно создать второго владельца копированием. Переход к std::shared_ptr оправдан не удобством, а требованием совместного владения.
До появления стандартных умных указателей управление динамической памятью часто выполнялось вручную через new и delete. Это приводило к утечкам, двойному освобождению и неясным правилам передачи владения между функциями.
Подход RAII связывает ресурс со временем жизни объекта: деструктор освобождает ресурс автоматически. std::unique_ptr и std::shared_ptr решают разные задачи внутри этой модели: первый выражает исключительное владение, второй — совместное.
Если использовать std::shared_ptr для ресурса, которым фактически владеет один объект, код начинает допускать дополнительные копии владельца. Время жизни ресурса становится зависимым от того, где ещё сохранились копии, а не только от владельца, которому ресурс логически принадлежит.
Это увеличивает стоимость управления: обычно требуется счётчик владельцев и управляющий блок. Кроме того, совместное владение может скрыть ошибку проектирования: объект неожиданно продолжает жить после уничтожения предполагаемого владельца.
Обратная ошибка также опасна: применение std::unique_ptr там, где ресурс должен переживать исходного владельца и использоваться несколькими компонентами, приводит к неудобной или некорректной передаче владения.
std::unique_ptr<T> означает, что в каждый момент времени существует не более одного владельца ресурса. Такой указатель нельзя копировать, но можно перемещать. Перемещение явно передаёт владение, а после него исходный std::unique_ptr становится пустым.
std::shared_ptr<T> хранит владение через управляющий блок со счётчиком сильных владельцев. Копирование увеличивает счётчик, а уничтожение или сброс копии уменьшает его. Объект уничтожается, когда исчезает последний сильный владелец; поэтому shared_ptr не выражает, какой именно объект является главным владельцем.
Минимальный пример различия:
В первом случае ресурс передаётся функции через явное перемещение. Во втором две переменные являются владельцами одного объекта, поэтому его время жизни определяется последней из них.
Практическое правило: по умолчанию начинать с std::unique_ptr, а std::shared_ptr вводить только при доказанной необходимости совместного владения. Если функции нужно лишь временно использовать объект, ей обычно передают ссылку или сырой указатель с явно оговорённым невладеющим смыслом, а не создают дополнительного владельца.
std::shared_ptr не следует выбирать только потому, что он копируемый. Копируемость может скрыть архитектурную ошибку. Также нельзя создавать отдельный shared_ptr из сырого указателя, уже управляемого другим shared_ptr: это создаёт независимый управляющий блок и может привести к двойному уничтожению.
В сервисе есть кэш изображений. Каждый запрос получает изображение, а после обработки оно больше не должно использоваться. Изначально изображение хранили в std::shared_ptr, потому что его было удобно копировать между слоями.
Рассматривались два варианта. Сохранить shared_ptr было проще для существующего кода, но копии продлевали жизнь изображений, усложняли оценку потребления памяти и не отражали реальную модель: передача между слоями была последовательной, а не совместной.
Переход на std::unique_ptr сделал передачу владения явной. Функция, которая завершает обработку, получает объект перемещением; промежуточные функции используют ссылку без изменения владения. В результате ресурс освобождается предсказуемо после завершения последнего владельца, а случайное сохранение копии невозможно на уровне типов.
Если бы изображение действительно использовалось несколькими независимыми подсистемами одновременно, std::shared_ptr был бы обоснованным выбором. Но тогда совместное владение следовало бы зафиксировать в интерфейсах и отдельно проверить отсутствие циклов владения.
Вопрос: Достаточно ли заменить std::shared_ptr на std::unique_ptr, если объект используют несколько функций?
Нет. Несколько функций могут пользоваться объектом без владения: им передают ссылку или сырой указатель, пока вызывающий код гарантирует достаточное время жизни. std::unique_ptr запрещает только копирование владельца; он не запрещает временный доступ к самому объекту.
Если же функции должны независимо сохранять право продлевать время жизни объекта, std::unique_ptr не подходит без изменения архитектуры. В таком случае нужен явный механизм совместного владения, например std::shared_ptr, либо иной владелец более высокого уровня.
Вопрос: Почему выбор std::shared_ptr может ухудшить не только производительность, но и дизайн интерфейса?
Параметр типа std::shared_ptr<T> сообщает вызывающему, что функция принимает или сохраняет долю владения. Если функция лишь временно читает объект, такой параметр создаёт ложное требование: вызывающий вынужден оформлять владение, хотя оно ему не нужно.
Для временного доступа лучше использовать T&, const T& или явно невладеющий указатель. Тип параметра тем самым документирует контракт: ссылка означает доступ, unique_ptr — исключительное владение или его передачу, shared_ptr — совместное владение.
Вопрос: Может ли std::unique_ptr быть менее подходящим из-за стоимости пользовательского deleter?
Да, но это не отменяет его преимуществ автоматически. Пользовательский deleter является частью типа std::unique_ptr; если он хранит состояние, размер указателя может увеличиться, а типы с разными deleter несовместимы напрямую.
Однако std::shared_ptr также хранит deleter в управляющем блоке и обычно всё равно требует отдельного управляющего состояния. Поэтому сравнивать нужно конкретные требования: размер объекта, способ передачи, необходимость совместного владения и правила освобождения ресурса. Сам факт наличия нестандартного deleter не является достаточной причиной переходить на shared_ptr.