Подтвердить что ты не робот

Почему в реализации std:: shared_ptr требуется два необработанных указателя на управляемый объект?

Здесь приведена цитата из раздела примечания реализации cppreference в std::shared_ptr, в которой упоминается, что существуют два разных указателя (как показано жирным шрифтом): тот, который может быть возвращен get(), и тот, который содержит фактические данные внутри блок управления.

В типичной реализации std::shared_ptr содержит только два указателя:

  • сохраненный указатель (один возвращается get())
  • указатель на блок управления

Управляющий блок представляет собой объект с динамическим распределением, который содержит:

  • либо указатель на управляемый объект, либо сам управляемый объект
  • удаляющий (стираемый тип)
  • распределитель (тип-стертый)
  • количество shared_ptrs, которому принадлежит управляемый объект
  • число weak_ptrs, относящееся к управляемому объекту

Указатель, удерживаемый shared_ptr напрямую, - это тот, который возвращается get(), в то время как указатель или объект, удерживаемый блоком управления, является тем, который будет удален, когда количество общих владельцев достигнет нуля. Эти указатели не обязательно равны.

Мой вопрос: зачем нужны два разных указателя (выделенные жирным шрифтом) для управляемого объекта (в дополнение к указателю на блок управления)? Достаточно ли того, что возвращается get()? И почему эти указатели не обязательно равны?

4b9b3361

Ответ 1

Причиной этого является то, что вы можете иметь shared_ptr, который указывает на что-то еще, чем то, что он владеет, и это по дизайну. Это реализовано с помощью конструктора, указанного как nr. 8 на cppreference:

template< class Y >
shared_ptr( const shared_ptr<Y>& r, T *ptr );

A shared_ptr, созданный с этим конструктором, имеет право собственности на r, но указывает на ptr. Рассмотрим этот (надуманный, но иллюстрирующий) код:

std::shared_ptr<int> creator()
{
  using Pair = std::pair<int, double>;

  std::shared_ptr<Pair> p(new Pair(42, 3.14));
  std::shared_ptr<int> q(p, &(p->first));
  return q;
}

После выхода этой функции клиентскому коду доступен только указатель на подобъект int пары. Но из-за совместного использования между q и p указатель q "сохраняет" весь объект Pair.

Как только произойдет деллалокация, указатель на весь объект Pair должен быть передан делетеру. Следовательно, указатель на объект Pair должен храниться где-то рядом с делетером - другими словами, в блоке управления.

Для менее надуманного примера (возможно, даже ближе к оригинальной мотивации для этой функции) рассмотрим случай указания на базовый класс. Что-то вроде этого:

struct Base1
{
  // :::
};

struct Base2
{
  // :::
};

struct Derived : Base1, Base2
{
 // :::
};

std::shared_ptr<Base2> creator()
{
  std::shared_ptr<Derived> p(new Derived());
  std::shared_ptr<Base2> q(p, static_cast<Base2*>(p.get()));
  return q;
}

Конечно, реальная реализация std::shared_ptr имеет все неявные преобразования на месте, так что танец p -and- q в creator не нужен, но я сохранил его там, чтобы напоминать первый пример.

Ответ 2

Одна неизбежная потребность в блоке управления заключается в поддержке слабых указателей. Не всегда возможно оповестить все слабые указатели об уничтожении объекта (на самом деле он почти всегда неосуществим). Соответственно, слабым указателям нужно что-то сказать, пока они не уйдут. Таким образом, некоторый блок памяти должен повесить. Этот блок памяти является блоком управления. Иногда они могут быть распределены вместе, но выделение их отдельно позволяет вам вернуть потенциально дорогостоящий объект, сохраняя при этом дешевый блок управления.

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

Это также допускает случаи, когда объект передается в совместное владение после его выделения. make_shared может объединить эти два понятия в один блок памяти, но shared_ptr<T>(new T) должен сначала выделить T, а затем выяснить, как разделить его после факта. Когда это нежелательно, boost имеет связанную концепцию intrusive_ptr, которая делает свой подсчет ссылок непосредственно внутри объекта, а не с помощью блока управления (вы должны сами писать операторы приращения и декремента, чтобы сделать эту работу).

Я видел общие реализации указателей, которые не имеют блока управления. Вместо этого общие указатели создают взаимосвязанный список между собой. Пока связанный список содержит 1 или более shared_ptrs, объект все еще жив. Однако этот подход более сложный в сценарии многопоточности, потому что вам нужно поддерживать связанный список, а не просто простой счетчик ссылок. Его время выполнения также, вероятно, будет хуже во многих сценариях, когда вы назначаете и повторно назначаете shared_ptrs повторно, потому что связанный список более тяжелый.

Также возможно, чтобы высокопроизводительная реализация пула распределяла блоки управления, заставляя их использовать почти до нуля.

Ответ 3

Посмотрите на std::shared_ptr<int> Это ссылочный подсчет интеллектуального указателя на int*. Теперь int* не содержит никакой информации подсчета ссылок, а сам объект shared_ptr не может содержать информацию подсчета ссылок, так как она может быть хорошо разрушена до того, как счетчик ссылок упадет до нуля.

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

Сказав, что если вы создаете shared_ptr с make_shared, то и int, и блок управления будут созданы в непрерывной памяти, что делает разыменование гораздо более эффективным.