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

Как реализуется std:: tr1:: shared_ptr?

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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 должен произойти только тогда, когда оба счетчика достигнут нуля.

Ответ 2

Как выполняется эталонный счет?

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

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

  • Политика владения

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

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

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

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

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

введите описание изображения здесь

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

введите описание изображения здесь

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

    введите описание изображения здесь

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

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

введите описание изображения здесь

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

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

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

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

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

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

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

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

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

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


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

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

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

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

Ответ 3

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

https://github.com/boostorg/smart_ptr

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

Ответ 4

Есть ли подводные камни для использования 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, если вы не слишком осторожны, может привести к проблемам.