Почему размер 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 было бы простым, наоборот, очень сложно. Даже если вы решите эту проблему эффективно, небольшая эффективность, которую вы получите, вероятно, будет потеряна благодаря конверсиям, которые неизбежно посыпаются в любую серьезную программу. И дополнительный тип указателя также делает код, который использует его сложнее, чтобы понять.