Как работают общие указатели? - PullRequest
36 голосов
/ 10 мая 2010

Как общие указатели узнают, сколько указателей указывают на этот объект? (shared_ptr, в данном случае)

Ответы [ 5 ]

59 голосов
/ 10 мая 2010

По сути, shared_ptr имеет два указателя: указатель на общий объект и указатель на структуру, содержащую два счетчика ссылок: один для «сильных ссылок» или ссылок, которые имеют право собственности, и один для «слабых ссылок» или ссылки, которые не имеют права собственности.

Когда вы копируете shared_ptr, конструктор копирования увеличивает счетчик сильных ссылок. Когда вы уничтожаете shared_ptr, деструктор уменьшает счетчик строгих ссылок и проверяет, равен ли счетчик нулей; если это так, деструктор удаляет общий объект, потому что shared_ptr s больше не указывает на него.

Слабый счетчик ссылок используется для поддержки weak_ptr; в основном, каждый раз, когда weak_ptr создается из shared_ptr, счетчик слабых ссылок увеличивается, а каждый раз, когда уничтожается, счетчик слабых ссылок уменьшается. Пока либо счетчик сильных ссылок, либо счетчик слабых ссылок больше нуля, структура счетчика ссылок не будет уничтожена.

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

16 голосов
/ 10 мая 2010

Я в целом согласен с ответом Джеймса Макнеллиса. Однако стоит упомянуть еще один момент.

Как вы, возможно, знаете, shared_ptr<T> также может использоваться, когда тип T определен не полностью.

То есть:

class AbraCadabra;

boost::shared_ptr<AbraCadabra> myPtr;
// ...

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

Это достигается с помощью следующего трюка: shared_ptr на самом деле состоит из следующего:

  1. Непрозрачный указатель на объект
  2. Общие счетчики ссылок (что описал Джеймс МакНеллис)
  3. Указатель на выделенную фабрику , которая знает, как уничтожить ваш объект.

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

Эта фабрика фактически создается, когда вы присваиваете значение для вашего общего указателя.

То есть следующий код

AbraCadabra* pObj = /* get it from somewhere */;
myPtr.reset(pObj);

Здесь находится эта фабрика. Примечание: функция reset на самом деле является функцией template . Это фактически создает фабрику для указанного типа (тип объекта, передаваемого в качестве параметра). Здесь ваш тип должен быть полностью определен. То есть, если он все еще не определен - вы получите ошибку компиляции.

Также обратите внимание: если вы на самом деле создаете объект производного типа (производного от AbraCadabra) и присваиваете ему shared_ptr - он будет удален правильным образом, даже если ваш деструктор не является виртуальным. shared_ptr всегда удаляет объект в соответствии с типом, который видит в функции reset.

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

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

Плюсы shared_ptr по сравнению с навязчивыми умными указателями:

  • Очень гибкое использование. Определять инкапсулированный тип нужно только при назначении его shared_ptr. Это очень ценно для больших проектов, значительно уменьшает зависимости.
  • Инкапсулированный тип не должен иметь виртуального деструктора, однако полиморфные типы будут удаляться корректно.
  • Может использоваться со слабыми указателями.

Минусы shared_ptr по сравнению с навязчивыми умными указателями:

  1. Очень варварская производительность и потеря кучи памяти. При назначении выделяется еще 2 объекта: счетчики ссылок плюс заводские (растрата памяти, медленный). Это, однако, происходит только на reset. Когда один shared_ptr назначен другому - больше ничего не выделяется.
  2. Вышеуказанное может вызвать исключение. (нехватка памяти). Напротив, интрузивные умные указатели могут никогда не выдавать (кроме исключений процесса, связанных с неправильным доступом к памяти, переполнением стека и т. Д.)
  3. Удаление вашего объекта также происходит медленно: необходимо освободить еще две структуры.
  4. При работе с навязчивыми умными указателями вы можете свободно смешивать умные указатели с необработанными. Это нормально, потому что фактический счетчик ссылок находится внутри самого объекта, который является единичным. Для сравнения: с shared_ptr вы можете , а не смешивать с необработанными указателями.

    AbraCadabra * pObj = / * получить его откуда-то * /; myPtr.reset (PObj); // ... pObj = myPtr.get (); boost :: shared_ptr myPtr2 (pObj); // упс

Выше будет сбой.

11 голосов
/ 10 мая 2010

Существует как минимум три известных механизма.

Внешние счетчики

Когда создается первый общий указатель на объект, создается отдельный объект подсчета ссылок, который инициализируется равным 1. Когда указатель копируется, счетчик ссылок увеличивается; когда указатель уничтожается, он уменьшается. Назначение указателя увеличивает один счет и уменьшает другой (в этом порядке, иначе само-назначение ptr=ptr прервется). Если число ссылок достигает нуля, указателей больше не существует, и объект удаляется.

Внутренние счетчики

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

Круглые ссылки

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

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

Изменения

2-я и 3-я идея могут быть объединены: базовый класс может быть частью этого кругового графа, а не содержать счетчик. Конечно, это означает, что объект может быть удален только тогда, когда он указывает на себя (длина цикла 1, без указателей на него). Опять же, преимущество состоит в том, что вы можете создавать умные указатели из слабых указателей, но низкая производительность удаления указателя из цепочки остается проблемой.

Точная структура графа для идеи 3 не имеет большого значения. Вы также можете создать бинарную древовидную структуру с указанным объектом в корне. Опять же, сложная операция - удаление узла общего указателя из этого графа. Преимущество состоит в том, что если у вас много указателей во многих потоках, растущая часть графа не является требовательной операцией.

0 голосов
/ 10 мая 2010

"Общий указатель - это интеллектуальный указатель (объект C ++ с перегруженным оператором * () и оператором -> ()), который хранит указатель на объект и указатель на общий счетчик ссылок. Каждый раз при копировании смарт-объекта указатель создается с помощью конструктора копирования, счетчик ссылок увеличивается. Когда общий указатель уничтожается, счетчик ссылок для его объекта уменьшается. Общие указатели, созданные из необработанных указателей, изначально имеют счетчик ссылок 1. Когда счетчик ссылок достигает 0 , указанный объект уничтожен, а занимаемая им память освобождена. Вам не нужно явно уничтожать объекты: это будет выполнено автоматически при запуске деструктора последнего указателя. " С здесь .

0 голосов
/ 10 мая 2010

Они содержат внутренний счетчик ссылок, который увеличивается в конструкторе / операторе копирования copy_ptr и уменьшается в деструкторе. Когда счет достигает нуля, удерживаемый указатель удаляется.

Вот библиотека Boost Документация для интеллектуальных указателей. Я думаю, что реализация TR1 в основном такая же, как boost::shared_ptr.

...