Когда использовать shared_ptr и когда использовать необработанные указатели?
class B;
class A
{
public:
A ()
: m_b(new B())
{
}
shared_ptr<B> GimmeB ()
{
return m_b;
}
private:
shared_ptr<B> m_b;
};
Пусть говорят, что B - это класс, который семантически не должен существовать вне времени жизни A, т.е. абсолютно не имеет смысла, чтобы B существовал сам по себе. Если GimmeB
возвращает a shared_ptr<B>
или B*
?
В целом, хорошая практика - полностью избегать использования необработанных указателей в коде на С++ вместо интеллектуальных указателей?
Я придерживаюсь мнения, что shared_ptr
следует использовать только при явной передаче или совместном использовании собственности, что, я думаю, довольно редко происходит за пределами случаев, когда функция выделяет некоторую память, заполняет ее некоторыми данными и возвращает это, и есть понимание между вызывающим и вызываемым, что первый теперь "ответственен" за эти данные.
Ответы
Ответ 1
Ваш анализ вполне правильный, я думаю. В этой ситуации я также вернул бы голый B*
или даже [const] B&
, если объект никогда не будет равен нулю.
Имея некоторое время для изучения интеллектуальных указателей, я пришел к некоторым рекомендациям, которые рассказывают мне, что делать во многих случаях:
- Если вы возвращаете объект, срок службы которого должен управляться вызывающим абонентом, верните
std::unique_ptr
. Вызывающий может назначить его std::shared_ptr
, если он хочет.
- Возвращение
std::shared_ptr
на самом деле довольно редко, и, когда это имеет смысл, это обычно очевидно: вы указываете вызывающему, что он продлит время жизни объекта, указанного за объектом, за время жизни объекта, который первоначально поддерживал ресурс. Возвращение общих указателей с фабрик не является исключением: вы должны это сделать, например. когда вы используете std::enable_shared_from_this
.
- Вам очень редко требуется
std::weak_ptr
, за исключением случаев, когда вы хотите понять метод lock
. У этого есть некоторые виды использования, но они редки. В вашем примере, если время жизни объекта A
не было детерминированным с точки зрения вызывающего, это было бы чем-то, что нужно учитывать.
- Если вы вернете ссылку на существующий объект, время жизни которого вызывающий не может контролировать, верните пустой указатель или ссылку. Поступая таким образом, вы говорите вызывающему, что объект существует и что ей не нужно заботиться о его жизни. Вы должны вернуть ссылку, если вы не используете значение
nullptr
.
Ответ 2
Вопрос "когда следует использовать shared_ptr
и когда следует использовать необработанные указатели?" имеет очень простой ответ:
- Используйте необработанные указатели, если вы не хотите, чтобы какое-либо право собственности было привязано к указателю. Эта работа также часто может выполняться со ссылками. Необработанные указатели также могут использоваться в некоторых низкоуровневых кодах (например, для реализации интеллектуальных указателей или реализации контейнеров).
- Используйте
unique_ptr
или scope_ptr
, если вам нужна уникальная собственность на объект. Это самый полезный вариант, и его следует использовать в большинстве случаев. Уникальное право собственности также можно выразить простым созданием объекта напрямую, вместо использования указателя (это даже лучше, чем использование unique_ptr
, если это можно сделать).
- Используйте
shared_ptr
или intrusive_ptr
, если вам требуется совместное владение указателем. Это может быть запутанным и неэффективным, и часто это не очень хороший вариант. Совместное владение может быть полезно в некоторых сложных проектах, но его следует избегать в целом, потому что это приводит к тому, что код трудно понять.
shared_ptr
выполнить совершенно другую задачу из исходных указателей, и ни shared_ptr
, ни исходные указатели не являются лучшим вариантом для большинства кодов.
Ответ 3
Вот хорошее эмпирическое правило:
- Когда нет передачи или совместного владения, ссылки или простые указатели достаточно хороши. (Простые указатели более гибкие, чем ссылки.)
- Когда есть передача права собственности, но нет совместного владения, тогда
std::unique_ptr<>
является хорошим выбором. Часто бывает с заводскими функциями. - Когда есть совместное владение, тогда это хороший вариант использования для
std::shared_ptr<>
или boost::intrusive_ptr<>
.
Лучше всего избегать совместного владения, отчасти потому, что они являются самыми дорогими с точки зрения копирования, а std::shared_ptr<>
занимает вдвое больше места хранения простого указателя, но, что наиболее важно, потому что они способствуют плохим проектам, где есть нет явных владельцев, что, в свою очередь, приводит к тому, что объекты не могут разрушаться, потому что они имеют общие указатели друг на друга.
Лучший дизайн - это то, где установлена четкая собственность и иерархическая структура, поэтому в идеале умные указатели вообще не требуются. Например, если существует фабрика, которая создает уникальные объекты или возвращает существующие, имеет смысл для фабрики владеть создаваемыми объектами и просто хранить их по значению в ассоциативном контейнере (например, std::unordered_map
), чтобы он может возвращать простые указатели или ссылки на своих пользователей. Эта фабрика должна иметь время жизни, которое начинается до ее первого пользователя и заканчивается после его последнего пользователя (иерархическое свойство), чтобы пользователи не могли иметь указатель на уже уничтоженный объект.
Ответ 4
Если вы не хотите, чтобы вызывающий GimmeB() мог продлить время жизни указателя, сохранив копию ptr после того, как экземпляр A умирает, вы определенно не должны возвращать shared_ptr.
Если вызываемый вызов не должен удерживать возвращаемый указатель в течение длительных периодов времени, то есть нет риска того, что экземпляр времени жизни A истекает до указателя, тогда исходный указатель будет лучше. Но даже лучший выбор - просто использовать ссылку, если нет веских оснований для использования фактического необработанного указателя.
И, наконец, в случае, если возвращаемый указатель может существовать после того, как срок жизни экземпляра A истек, но вы не хотите, чтобы сам указатель увеличивал время жизни B, тогда вы можете вернуть weak_ptr, который вы можете использовать, чтобы проверить, существует ли он еще.
Суть в том, что обычно есть более приятное решение, чем использование необработанного указателя.
Ответ 5
Я согласен с вашим мнением, что shared_ptr
лучше всего использовать при явном совместном использовании ресурсов, однако существуют и другие типы интеллектуальных указателей.
В вашем конкретном случае: почему бы не вернуть ссылку?
Указатель предполагает, что данные могут быть нулевыми, однако в вашем A
всегда будет B
, поэтому он никогда не будет равен нулю. Ссылка ссылается на это поведение.
Как я уже сказал, я видел людей, которые выступают за использование shared_ptr
даже в не-общих средах и предоставляют дескрипторы weak_ptr
, с идеей "обеспечения безопасности" приложения и исключения устаревших указателей. К сожалению, поскольку вы можете восстановить shared_ptr
из weak_ptr
(и это единственный способ фактически манипулировать данными), это по-прежнему совместное владение, даже если оно не должно быть.
Примечание: есть тонкая ошибка с shared_ptr
, копия A
по умолчанию будет использовать те же B
, что и оригинал, если вы явно не пишете конструктор копирования и оператор присваивания копии. И, конечно, вы бы не использовали необработанный указатель в A
для хранения B
, не могли бы вы:)?
Конечно, еще один вопрос: действительно ли вам нужно это делать. Одним из принципов хорошего дизайна является инкапсуляция. Чтобы добиться инкапсуляции:
Вы не должны возвращать ручки своим внутренним частям (см. Закон Деметры).
поэтому, возможно, реальный ответ на ваш вопрос заключается в том, что вместо того, чтобы отсылать ссылку или указатель на B
, ее следует изменять только через интерфейс A
.
Ответ 6
В общем, я бы избегал использовать необработанные указатели, насколько это возможно, поскольку они имеют очень двусмысленный смысл - вам, возможно, придется освободить плательщика, но, возможно, нет, и только документация, написанная человеком и написанная, сообщает вам, в чем дело. И документация всегда плохая, устаревшая или неправильно понятая.
Если право собственности является проблемой, используйте интеллектуальный указатель. Если нет, я бы использовал ссылку, если это практически осуществимо.
Ответ 7
- Вы выделяете B при построении A.
- Вы говорите, что B не должен оставаться снаружи. Как жизнь.
Оба они указывают на то, что B является членом A и просто возвращает ссылочный аксессуар. Вы переоцениваете это?
Ответ 8
Хорошая практика - избегать использования необработанных указателей, но вы не можете просто заменить все на shared_ptr
. В этом примере пользователи вашего класса предполагают, что это нормально продлить время жизни B за пределами жизни A и может принять решение о возврате возвращаемого объекта B в течение некоторого времени по их собственным причинам. Вы должны вернуть weak_ptr
, или, если B абсолютно не может существовать, когда A уничтожен, ссылка на B или просто необработанный указатель.
Ответ 9
Я обнаружил, что в Основных рекомендациях C++ есть несколько очень полезных советов по этому вопросу:
Использование необработанного указателя (T *) или более умного указателя зависит от того, кому принадлежит объект (чья ответственность освобождать память объекта).
своя:
smart pointer, owner<T*>
не владеть:
T*, T&, span<>
владелец <>, span <> определен в библиотеке Microsoft GSL
вот правила большого пальца:
1) никогда не используйте необработанный указатель (или не собственные типы) для передачи права собственности
2) умный указатель должен использоваться только тогда, когда семантика владения предназначена
3) Т * или владелец обозначают отдельный объект (только)
4) использовать вектор/массив/диапазон для массива
5) На мой взгляд, shared_ptr обычно используется, когда вы не знаете, кто выпустит объект, например, один объект используется многопоточностью.
Ответ 10
Когда вы говорите: "Пусть говорят, что B - это класс, который семантически не должен существовать вне времени жизни A"
Это говорит мне, что B должен логически не существовать без A, но как насчет физически существующего?
Если вы можете быть уверены, что никто не будет пытаться использовать * B после A dtors, чем, возможно, необработанный указатель будет в порядке. В противном случае может оказаться целесообразным указатель.
Когда у клиентов есть прямой указатель на A, вы должны доверять, что они будут обращаться с ним соответствующим образом; не пытайтесь использовать его и т.д.