Почему размер make_shared двух указателей?
Как показано в коде здесь, размер объекта, возвращаемого из make_shared, является двумя указателями.
Однако почему make_shared
работает следующим образом (предположим, что T - это тип, которым мы располагаем общий указатель):
Результат make_shared
- это указатель one, который указывает на выделенную память размером sizeof(int) + sizeof(T)
, где int - счетчик ссылок, и это увеличивается и уменьшается на построение/уничтожение указателей.
unique_ptr
- это только размер одного указателя, поэтому я не уверен, почему для общего указателя нужны два. Насколько я могу судить, все, что ему нужно, - это счетчик ссылок, который с make_shared
может быть помещен вместе с самим объектом.
Кроме того, существует ли какая-либо реализация, которая реализована так, как я предлагаю (без необходимости обманывать intrusive_ptr
для определенных объектов)? Если нет, то почему я предлагаю исключение?
Ответы
Ответ 1
Во всех реализациях, о которых я знаю, shared_ptr
хранит принадлежащий ему указатель и счетчик ссылок в том же блоке памяти. Это противоречит тому, что говорят другие ответы. Кроме того, копия указателя будет сохранена в объекте shared_ptr
. N1431 описывает типичный макет памяти.
Верно, что можно построить указатель подсчета ссылок с размером только одного указателя. Но std::shared_ptr
содержит функции, которые абсолютно требуют размера двух указателей. Одной из этих особенностей является этот конструктор:
template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;
Effects: Constructs a shared_ptr instance that stores p
and shares ownership with r.
Postconditions: get() == p && use_count() == r.use_count()
Один указатель в shared_ptr
будет указывать на блок управления, принадлежащий r
. Этот блок управления будет содержать принадлежащий ему указатель, который не должен быть p
, и обычно это не p
. Другой указатель в shared_ptr
, возвращаемый get()
, будет p
.
Это называется поддержкой псевдонимов и введено в N2351. Вы можете заметить, что shared_ptr
имел размер двух указателей перед введением этой функции. До введения этой функции можно было бы реализовать shared_ptr
с размером одного указателя, но никто не сделал, потому что это было непрактично. После N2351 это стало невозможно.
Одна из причин, по которой это было нецелесообразно до N2351, вызвано поддержкой:
shared_ptr<B> p(new A);
Здесь p.get()
возвращает B*
и вообще забыл о типе A
. Единственное требование - преобразовать A*
в B*
. B
может быть получен из A
с использованием множественного наследования. И это означает, что значение самого указателя может измениться при преобразовании с A
в B
и наоборот. В этом примере shared_ptr<B>
необходимо запомнить две вещи:
- Как вернуть
B*
при вызове get()
.
- Как удалить
A*
, когда наступит время.
Очень хороший способ реализации для этого состоит в том, чтобы сохранить B*
в объекте shared_ptr
и A*
внутри блока управления со счетчиком ссылок.
Ответ 2
Счетчик ссылок не может быть сохранен в shared_ptr
. shared_ptr
должны совместно использовать счетчик ссылок среди различных экземпляров, поэтому shared_ptr
должен иметь указатель на счетчик ссылок. Кроме того, shared_ptr
(результат make_shared
) не нужно хранить счетчик ссылок в том же распределении, в котором был выделен объект.
Точкой make_shared
является предотвращение выделения двух блоков памяти для shared_ptr
s. Обычно, если вы просто выполняете shared_ptr<T>(new T())
, вам необходимо выделить память для счетчика ссылок в дополнение к выделенному T
. make_shared
помещает все это в один блок распределения, используя размещение new и delete для создания T
. Таким образом, вы получаете только одно распределение памяти и одно удаление.
Но shared_ptr
должен по-прежнему иметь возможность хранить счетчик ссылок в другом блоке памяти, поскольку использование make_shared
не требуется. Поэтому ему нужны два указателя.
Действительно, это не должно вас беспокоить. Два указателя - это не так много места, даже в 64-битной земле. Вы по-прежнему получаете важную часть функциональности intrusive_ptr
(а именно, не выделяя память дважды).
Ваш вопрос кажется "почему бы make_shared
вернуть shared_ptr
вместо какого-либо другого типа?" Существует много причин.
shared_ptr
предназначен для того, чтобы быть своего рода дефолтным, уловимым умным указателем. Вы можете использовать unique_ptr или scoped_ptr для случаев, когда вы делаете что-то особенное. Или просто для распределения временной памяти в области функций. Но shared_ptr
предназначен для того, чтобы вы использовали какую-либо серьезную ссылку на подсчитанную работу.
Из-за этого shared_ptr
будет частью интерфейса. У вас будут функции, которые принимают shared_ptr
. У вас будут функции, возвращающие shared_ptr
. И так далее.
Введите make_shared
. По вашей идее, эта функция вернет некоторый новый вид объекта, make_shared_ptr
или что-то еще. Он будет иметь свой собственный эквивалент weak_ptr
, a make_weak_ptr
. Но, несмотря на то, что эти два набора типов будут использовать один и тот же интерфейс, вы не сможете использовать их вместе.
Функции, которые принимают make_shared_ptr
, не могут принимать shared_ptr
. Вы можете сделать make_shared_ptr
конвертируемым в shared_ptr
, но вы не можете пойти наоборот. Вы не смогли бы взять любой shared_ptr
и превратить его в make_shared_ptr
, потому что shared_ptr
должен иметь два указателя. Он не может выполнять свою работу без двух указателей.
Итак, теперь у вас есть два набора указателей, которые наполовину несовместимы. У вас есть односторонние конверсии; если у вас есть функция, которая возвращает shared_ptr
, пользователю лучше использовать shared_ptr
вместо make_shared_ptr
.
Выполнение этого ради ценности указателя просто не стоит. Создавая эту несовместимость, создавая два набора указателей всего за 4 байта? Это просто не стоит того, что вызвано.
Теперь, возможно, вы спросите: "Если у вас есть make_shared_ptr
, зачем вам вообще нужно shared_ptr
?
Потому что make_shared_ptr
недостаточно. make_shared
- это не единственный способ создать shared_ptr
. Возможно, я работаю с C-кодом. Возможно, я использую SQLite3. sqlite3_open
возвращает sqlite3*
, который является подключением к базе данных.
Прямо сейчас, используя правый функтор деструктора, я могу сохранить это sqlite3*
в shared_ptr
. Этот объект будет считаться ссылкой. При необходимости я могу использовать weak_ptr
. Я могу сыграть все трюки, которые я обычно использовал бы с обычным С++ shared_ptr
, который я получаю от make_shared
или любого другого интерфейса. И это будет работать отлично.
Но если make_shared_ptr
существует, то это не работает. Потому что я не могу создать один из них. sqlite3*
уже выделен; Я не могу пропустить его через make_shared
, потому что make_shared
создает объект. Он не работает с уже существующими.
О, конечно, я мог бы сделать взломать, где я собираю sqlite3*
в С++-типе, который деструктор уничтожит его, а затем используйте make_shared
для создания этого типа. Но тогда использование его становится намного сложнее: вам нужно пройти еще один уровень косвенности. И вам приходится сталкиваться с проблемой создания типа и так далее; метод деструктора выше, по крайней мере, может использовать простую лямбда-функцию.
Распространение типов интеллектуальных указателей - это то, чего следует избегать. Вам нужен неподвижный, подвижный и универсальный. И еще один, чтобы разбить круговые ссылки от последнего. Если вы начинаете иметь несколько из этих типов, то у вас либо есть особые потребности, либо вы делаете что-то неправильно.
Ответ 3
У меня есть реализация honey::shared_ptr
, которая автоматически оптимизируется до 1-го указателя при назойливости. Он концептуально прост - типы, наследующие от SharedObj
, имеют встроенный блок управления, поэтому в этом случае shared_ptr<DerivedSharedObj>
является навязчивым и может быть оптимизирован. Он объединяет boost::intrusive_ptr
с неинтрузивными указателями, такими как std::shared_ptr
и std::weak_ptr
.
Эта оптимизация возможна только потому, что я не поддерживаю псевдонимы (см. ответ Говарда). Результат make_shared
может затем иметь 1 размер указателя, если известно, что T
является интрузивным во время компиляции. Но что, если T
, как известно, является неинтрузивным во время компиляции? В этом случае нецелесообразно иметь 1 размер указателя, так как shared_ptr
должен вести себя в целом, чтобы поддерживать блоки управления, выделенные как рядом, так и отдельно от их объектов. Только с одним указателем общее поведение было бы указывать на блок управления, поэтому, чтобы добраться до T*
, вам придется сначала разыменовать контрольный блок, который непрактичен.
Ответ 4
Другие уже заявили, что для shared_ptr
нужны два указателя, потому что они должны указывать на блок памяти отсчета ссылок и блок памяти "Указанный на типы".
Я предполагаю, что вы спрашиваете:
При использовании make_shared
оба блока памяти объединяются в один, и поскольку размеры и выравнивание блоков известны и фиксированы во время компиляции, один указатель может быть рассчитан от другого (поскольку они имеют фиксированное смещение). Итак, почему стандарт или boost не создают второй тип типа small_shared_ptr
, который содержит только один указатель.
Это верно?
Хорошо, ответ заключается в том, что, если вы думаете, что через это быстро становится большой проблемой для очень небольшого выигрыша. Как вы можете сделать указатели совместимыми? Одно направление, то есть присвоение a small_shared_ptr
a shared_ptr
было бы простым, наоборот, очень сложно. Даже если вы решите эту проблему эффективно, небольшая эффективность, которую вы получите, вероятно, будет потеряна благодаря конверсиям, которые неизбежно посыпаются в любую серьезную программу. И дополнительный тип указателя также делает код, который использует его сложнее, чтобы понять.