Как реализован std :: tr1 :: shared_ptr? - PullRequest
33 голосов
/ 09 февраля 2012

Я думал об использовании общих указателей, и я знаю, как реализовать его самостоятельно - я не хочу этого делать, поэтому я пытаюсь std::tr1::shared_ptr, и у меня есть пара вопросов ...

Как осуществляется подсчет ссылок?Использует ли он двусвязный список?(Кстати, я уже гуглил, но не могу найти ничего надежного.)

Есть ли подводные камни для использования std::tr1::shared_ptr?

Ответы [ 4 ]

52 голосов
/ 09 февраля 2012

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

Класс shared_ptr обычно содержит два члена: T* (который возвращается operator-> и разыменовывается в operator*) и aux*, где aux - это внутренний абстрактный класс, который содержит:

  • счетчик (увеличивается / уменьшается при копировании-назначении / уничтожении)
  • все, что нужно для атомарного увеличения / уменьшения (не требуется, если доступна конкретная атомная платформа INC / DEC)
  • аннотация virtual destroy()=0;
  • виртуальный деструктор.

Такой aux класс (фактическое имя зависит от реализации) получен семейством шаблонизированных классов (параметризованных по типу, заданному явным конструктором, скажем, U, полученным из T), которые добавляют:

  • указатель на объект (такой же, как T*, но с фактическим типом: это необходимо для правильного управления всеми случаями, когда T является базой для любого U, имеющего несколько T в деривации иерархия) * * тысяча тридцать-три
  • копия объекта deletor, заданная в качестве политики удаления для явного конструктора (или по умолчанию deletor, просто выполняющая удаление p, где p - это U* выше)
  • переопределение метода уничтожения, вызывающего функтор удаления.

Упрощенный эскиз может быть таким:

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

Если требуется weak_ptr совместимость, необходим второй счетчик (weak_count) в aux (будет увеличен / уменьшен на weak_ptr), а delete pa должен произойти только тогда, когда оба счетчика достигнут нуля.

25 голосов
/ 04 февраля 2016

Как осуществляется подсчет ссылок?

Реализацию интеллектуального указателя можно деконструировать, используя дизайн класса на основе политик 1 , в:

  • Политика хранения

  • Право собственности

  • Политика конвертации

  • Политика проверки

включено в качестве параметров шаблона. Популярные стратегии владения включают: глубокое копирование, подсчет ссылок, связывание ссылок и деструктивное копирование.

Подсчет ссылок отслеживает количество умных указателей, указывающих (владеющих 2 ) на один и тот же объект. Когда число обращается в ноль, объект pointee удаляется 3 . Фактический счетчик может быть:

  1. Разделяется между объектами интеллектуального указателя, где каждый интеллектуальный указатель содержит указатель на счетчик ссылок:

enter image description here

  1. Включено только в дополнительную структуру, которая добавляет дополнительный уровень косвенности объекту pointee. Здесь затраты на хранение счетчика в каждом интеллектуальном указателе обмениваются с более низкой скоростью доступа:

enter image description here

  1. Содержится в самом объекте pointee: навязчивый подсчет ссылок. Недостатком является то, что объект должен быть построен априори с возможностями для подсчета:

    enter image description here

  2. Наконец, метод в вашем вопросе, подсчет ссылок с использованием двусвязных списков, называется связыванием ссылок, и это:

... [1] полагается на то, что вам на самом деле не нужно фактическое количество объектов интеллектуальных указателей, указывающих на один объект-указатель; вам нужно только определить, когда это количество уменьшается до нуля. Это приводит к идее ведения «списка владельцев»:

enter image description here

Преимущество связывания ссылок перед подсчетом ссылок состоит в том, что первое не использует дополнительное свободное хранилище, что делает его более надежным: создание интеллектуального указателя со ссылками не может завершиться с ошибкой. Недостатком является эта ссылочная ссылка требует больше памяти для ведения бухгалтерии (три указателя против только одного указателя плюс одно целое число). Кроме того, подсчет ссылок должен быть немного быстрее - когда вы копируете умные указатели, требуется только косвенное указание и приращение. Управление списком немного сложнее. В заключение, Вы должны использовать ссылки ссылки только тогда, когда в свободном магазине мало. В противном случае предпочитайте подсчет ссылок.

Относительно вашего второго вопроса:

Использует ли он (std::shared_ptr) двусвязный список?

Все, что я мог найти в стандарте C ++, было:

20.7.2.2.6 создание shared_ptr
...
7. [Примечание: Эти функции обычно выделяют больше памяти, чем sizeof(T), чтобы учесть внутренние структуры бухгалтерского учета, такие как счетчик ссылок. —Конечная записка]

Что, на мой взгляд, исключает двусвязные списки, поскольку они не содержат фактического количества.

Ваш третий вопрос:

Есть ли подводные камни для использования std::shared_ptr?

Управление ссылками, считая или связывая, является жертвой утечки ресурса, известной как циклическая ссылка . Давайте возьмем объект A, который содержит интеллектуальный указатель на объект B. Кроме того, объект B содержит интеллектуальный указатель на A. Эти два объекта образуют циклическую ссылку; даже если вы их больше не используете, они используют друг друга. Стратегия управления ссылками не может обнаружить такие циклические ссылки, и эти два объекта остаются распределенными навсегда.

Поскольку реализация shared_ptr использует подсчет ссылок, циклические ссылки потенциально являются проблемой.Циклическую цепочку shared_ptr можно разорвать, изменив код так, чтобы одна из ссылок была weak_ptr.Это делается путем назначения значений между общими указателями и слабыми указателями, но слабый указатель не влияет на счетчик ссылок.Если единственные указатели, которые указывают на объект, являются слабыми, объект уничтожается.


1.Каждая конструктивная особенность с несколькими реализациями, если сформулирована как политика.

2.Умные указатели, аналогичные указателям, указывающим на объект, выделенный с помощью new, не только указывают на этот объект, но и несут ответственность за его уничтожение и освобождение занимаемой памяти.

3.Без дальнейших проблем, если никакие другие необработанные указатели не используются и / или не указывают на это.

[1] Современный дизайн C ++: применяются общие шаблоны программирования и проектирования.Андрей Александреску, 1 февраля 2001 г.

4 голосов
/ 09 февраля 2012

Если вы хотите увидеть все кровавые подробности, вы можете взглянуть на реализацию shared_ptr boost:

https://github.com/boostorg/smart_ptr

Подсчет ссылок, как правило, осуществляется с помощью счетчиков и специфических для платформы атомарных инструкций увеличения / уменьшения или явной блокировки с помощью мьютекса (см. Файлы atomic_count_*.hpp в подробном пространстве имен ).

3 голосов
/ 09 февраля 2012

Есть ли подводные камни для использования std::tr1::shared_ptr?

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

Например:

struct A
{
    std::shared_ptr<A> ptr;
};

std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
shrd_ptr_1->ptr = shrd_ptr_2;
shrd_ptr_2->ptr = shrd_ptr_1;

Теперь, даже если shrd_ptr_1 и shrd_ptr_2 выходят из области видимости, память, которой они управляют, не освобождается, поскольку каждый элемент ptr указывает друг на друга.Хотя это очень наивный пример такого цикла памяти, он может, если вы используете эти типы указателей без какой-либо дисциплины, происходить гораздо более гнусным и трудно отслеживаемым образом.Например, я мог видеть, где попытка реализовать круговой связанный список, где каждый указатель next является std::shared_ptr, если вы не слишком осторожны, может привести к проблемам.

...