Почему enable_shared_from_this встраивает слабый указатель вместо непосредственного встраивания счетчика ссылок?

Помощник enable_shared_from_this содержит слабый указатель, который устанавливается при создании общего указателя на объект. Это означает, что есть счетчик ссылок (выделенный отдельно или вместе с объектом с помощью make_shared) и дополнительный weak_ptr в объекте.

Теперь, почему вместо этого он просто не содержит счетчик ссылок? При установке shared_ptr из немого указателя тип должен быть полностью определен, поэтому конструктор shared_ptr или оператор присваивания могут определить тип, полученный из enable_shared_from_this, и использовать правильный счетчик, а формат может оставаться прежним, поэтому копирование не имеет значения. На самом деле, shared_ptr уже должен его обнаружить, чтобы установить встроенный weak_ptr.


person Jan Hudec    schedule 26.07.2011    source источник


Ответы (3)


Первое, что приходит на ум, это то, что такой подход вообще осуществим, и ответ таков: нет:

struct X : enable_shared_from_this {};
std::shared_ptr<X> p( new X );
std::weak_ptr<X> w( p );
p.reset();                      // this deletes the object
if ( w.use_count() ) {          // this needs access to the count object
                                //    but it is gone! Undefined Behavior

Если счетчик хранится в объекте, то ни один weak_ptr не может пережить объект, что является нарушением контракта. Вся идея weak_ptr в том, что они могут пережить объект (если последний shared_ptr выходит за рамки, объект удаляется, даже если есть weak_ptr)

person David Rodríguez - dribeas    schedule 26.07.2011
comment
Первоначально я думал, что слабые указатели могут формировать связанный список и сбрасываться при удалении объекта, но выполнение этого потокобезопасным способом, очевидно, является настолько огромным червем, что это не очень жизнеспособно. - person Jan Hudec; 26.07.2011
comment
@Jan Hudec: Где бы ни хранился каждый weak_ptr, есть часть общих данных: активен ли объект, эти данные находятся в объекте count и не могут быть удалены до того, как последний weak_ptr выйдет из области действия/ удален. Обратите внимание, что weak_ptr не освобождаются, когда объект умирает, они могут пережить указанный объект (вся цель в том, что они могут). Я не думаю, что многопоточность имеет какое-либо отношение к этой проблеме. - person David Rodríguez - dribeas; 26.07.2011
comment
Да, я понимаю, что они реализованы таким образом. Проблема с потоками заключается в следующем: вы можете реализовать слабые указатели так, чтобы все слабые указатели на объект были связаны со связанным списком, и вы обнуляете их все, когда объекты уничтожаются, поэтому нет общей части, которая должна была бы остаться. Однако манипулирование списком потребует большого количества блокировок, что сделает этот подход чрезвычайно медленным в C++ (хотя его можно эффективно использовать в средах с остановкой сборки мусора или в средах с большой блокировкой интерпретатора, таких как python). - person Jan Hudec; 26.07.2011
comment
Вы можете сохранить структуру данных и уничтожить только объект. Не приятно, что размер объекта огромен. Вы можете выбрать различные реализации на основе sizeof. - person curiousguy; 08.10.2011
comment
@curiousguy: Вы говорите, что счетчик ссылок будет распределяться вместе, а не как часть объекта. Вы не можете сделать это с базовым классом, счетчик ссылок либо является частью класса, либо нет. Поскольку уничтожение object влечет за собой уничтожение базовых классов, это означает, что счетчик ссылок не может быть встроен в enable_shared_from_this. Преимущество, с другой стороны, будет заключаться в непрерывном распределении, и это уже может быть достигнуто с помощью make_shared (одиночное выделение, два объекта: счетчик ссылок/реальный объект). - person David Rodríguez - dribeas; 09.10.2011
comment
Хм... Вы могли бы встроить необработанное хранилище для внутренней структуры данных shared_ptr (как это называется?) в enable_shared_from_this. Акробатика, но весело. - person curiousguy; 09.10.2011
comment
@DavidRodríguez-dribeas Отсюда возник мой вопрос: Что происходит с подобъектами скалярного типа после уничтожения объекта? - person curiousguy; 31.08.2019

Разделение проблем: make_shared для встраивания счетчика, enable_shared_from_this для shared_from_this.

Нет причин смешивать эти два понятия: библиотека не может предположить, какие требования есть в клиентском коде. Разделив их, клиентский код может выбрать то, что подходит лучше всего.

Кроме того, Boost (откуда shared_ptr) также предлагает intrusive_ptr.

(Учтите, что ваше предложение, по-видимому, не позволяет использовать настраиваемые средства удаления. Вы могли бы исправить это, изменив enable_shared_from_this на template<typename T, typename Deleter = default_deleter<T>> class enable_shared_from_this;, но к этому моменту это уже близко к тому, чтобы заново изобрести intrusive_ptr.)

person Luc Danton    schedule 26.07.2011
comment
Если бы enable_shared_from_this встроил счетчик, это сэкономило бы место и стало бы более эффективным. Разделение интересов не является причиной для его реализации менее эффективным способом. И я не думаю, что это препятствует тому, чтобы пользовательское средство удаления проходило обычным способом (конструктору shared_ptr просто нужно было бы обернуть его немного по-другому, потому что он не должен удалять счетчик ссылок). - person Jan Hudec; 26.07.2011
comment
@Jan Вы задумывались о том, что у удаления могут быть состояния и разные размеры? Если вы сделаете shared_ptr<enabled_type>(new enabled_type, some_deleter()), сколько распределений будет сделано с вашей схемой? Если средство удаления выделяется отдельно, имеет ли значение, встроен ли счетчик ссылок внутрь объекта или живет вместе с средством удаления? - person Luc Danton; 26.07.2011
comment
Хм, вы правы, что общий случай удаления довольно сложный. Тем не менее, это может быть более эффективным в общем случае с удалением по умолчанию. - person Jan Hudec; 26.07.2011
comment
@Jan Вот почему я в первую очередь упомянул о разделении задач: make_shared действительно налагайте некоторые ограничения (например, никаких пользовательских средств удаления), это не оптимизация, доступная для каждой ситуации. Так же, как то, что вы предлагаете. - person Luc Danton; 26.07.2011
comment
@Luc Danton: я не читал реализацию shared_ptr, но я ожидаю, что она выполнит стирание типа в средстве удаления, что означает, что объект с удаленным типом будет одинаковым во всех объектах count. - person David Rodríguez - dribeas; 26.07.2011
comment
@David Это то, что он делает, да. Важно то, где он хранится в текущей реализации по сравнению с предложением Яна. - person Luc Danton; 26.07.2011
comment
@DavidRodríguez-dribeas Я не читал реализацию shared_ptr В настоящее время он использует виртуальную функцию; Я смутно припоминаю, что раньше для эффективности его реализовывали с помощью указателя на функцию. - person curiousguy; 10.10.2011
comment
@curiousguy: Это то, что называется стиранием типа, фактическое удаление скрыто в деталях реализации, и с точки зрения shared_ptr точный тип не имеет значения , тип был стерт. Стирание типа обычно реализуется в терминах базы с виртуальной функцией и одним или несколькими шаблонами производных классов, экземпляры которых создаются динамически, а наличие указателя/ссылки на базовый тип скрывает точную реализацию используемого шаблона. - person David Rodríguez - dribeas; 10.10.2011

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

person James Kanze    schedule 26.07.2011
comment
Нет, я не буду. Копирование дополнительного указателя дешево. Дорогостоящая вещь — это распределение. - person Jan Hudec; 26.07.2011
comment
Не могли бы вы объяснить проблемы? - person curiousguy; 08.10.2011
comment
@JanHudec Многое зависит от компилятора; в крайнем случае (но я не знаю такого компилятора) shared_ptr с одним указателем может быть передано в регистр, а shared_ptr с двумя указателями - нет. Что имело бы радикальное значение. В более общем случае копирование двух указателей будет стоить дороже, чем копирование одного. Но вы правы в том, что выделение стоит дорого, и другой важной проблемой является локальность — скорее всего, отдельный счетчик находится в другой строке кеша, что увеличит количество попаданий в кеш. Так что даже со вторым указателем вы можете много выиграть. - person James Kanze; 10.10.2011
comment
@curiousguy Я не уверен, о каких проблемах ты говоришь. Нет места для размещения объекта-деструктора, если вы встраиваете счетчик в объект, а не выделяете отдельный счетчик-деструктор, как это делает boost::shared_ptr. - person James Kanze; 10.10.2011
comment
Вы бы поместили точно то же самое в объект, который вы выделяете отдельно, так что нет, это не аргумент. - person Jan Hudec; 10.10.2011
comment
@JamesKanze Нет места для размещения объекта деструктора У вас может быть массив символов и применять оптимизацию только в том случае, если функтор подходит (подумайте: оптимизация короткой строки). Это не настоящая проблема. Настоящая проблема заключается в поддержке слабого подсчета: вам нужно будет разделить уничтожение объекта (что является наблюдаемым поведением) и освобождение памяти (и освобождение памяти не должно быть частью наблюдаемого поведения shared_ptr). - person curiousguy; 10.10.2011
comment
@curiousguy Настоящая проблема не является относительной; это, безусловно, значительно усложнило бы указатель. И поддержка слабых указателей — еще один хороший момент. - person James Kanze; 11.10.2011
comment
@JamesKanze Все это обсуждение убедило меня в том, что общий случай нельзя оптимизировать. Но я не уверен, что случай, когда деструктор не виртуальный, и нет перегруженного operator delete, а используется одноаргументный конструктор share_ptr нельзя оптимизировать так, чтобы не было выделения пока shared_ptr не будет преобразовано в weak_ptr. - person curiousguy; 11.10.2011