Хранитель shared_ptr хранится в памяти, выделенной пользовательским распределителем?

22

Скажем, у меня есть shared_ptrпользовательский распределитель и пользовательский удалитель.

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

Это не указано или я просто что-то упустил?

Гонки легкости на орбите
источник

Ответы:

11

util.smartptr.shared.const / 9 в C ++ 11:

Эффекты: Создает объект shared_ptr, которому принадлежит объект p и средство удаления d. Второй и четвертый конструкторы должны использовать копию a для выделения памяти для внутреннего использования.

Второй и четвертый конструкторы имеют эти прототипы:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

В последнем проекте util.smartptr.shared.const / 10 эквивалентен для нашей цели:

Эффекты: Создает объект shared_ptr, которому принадлежит объект p и средство удаления d. Когда T не является типом массива, первый и второй конструкторы разрешают shared_from_this с p. Второй и четвертый конструкторы должны использовать копию a для выделения памяти для внутреннего использования. Если выдается исключение, вызывается d (p).

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

  • Хотя интерфейс shared_ptrдопускает реализацию, в которой никогда не бывает блока управления и все shared_ptrи weak_ptrпомещены в связанный список, такой реализации на практике не существует. Кроме того, формулировка была изменена, предполагая, например, что она use_countявляется общей.

  • Средство удаления требуется только для перемещения конструктива. Таким образом, невозможно иметь несколько копий в shared_ptr.

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

Соответствующие DR, которые я нашел: 545 , 575 , 2434 (которые подтверждают, что все реализации используют блок управления и, по-видимому, подразумевают, что многопоточность ограничивает его), 2802 (который требует, чтобы средство удаления перемещалось только конструктивно, и, таким образом, предотвращал реализацию, где удалитель копируется между несколькими shared_ptr).

AProgrammer
источник
2
«выделить память для внутреннего использования» Что делать, если реализация не собирается выделять память для внутреннего использования для начала? Это может использовать член.
LF
1
@LF Не может, интерфейс не позволяет этого.
AProgrammer
Теоретически, он все еще может использовать какую-то «небольшую оптимизацию удаления», верно?
LF
Что странно, так это то, что я не могу найти что-либо об использовании одного и того же распределителя (копии a) для освобождения этой памяти. Что подразумевает некоторое хранение этой копии a. Об этом нет информации в [util.smartptr.shared.dest].
Даниэль Лангр
1
@DanielsaysreinstateMonica, мне интересно, если в util.smartptr.shared / 1: «Шаблон класса shared_ptr хранит указатель, обычно полученный через new. Shared_ptr реализует семантику общего владения; последний оставшийся владелец указателя отвечает за уничтожение объекта, или иным образом освобождая ресурсы, связанные с сохраненным указателем. " то освобождающие ресурсы , связанные с сохраненным указателем не предназначены для этого. Но контрольный блок также должен сохраняться до тех пор, пока последний слабый указатель не будет удален.
AProgrammer
4

Из std :: shared_ptr имеем:

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

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

И из std :: allocate_shared мы получаем:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Создает объект типа T и помещает его в std :: shared_ptr [...], чтобы использовать одно распределение как для блока управления общего указателя, так и для объекта T.

Так что похоже, что std :: allocate_shared должен выделять deleterвместе с вашим Alloc.

РЕДАКТИРОВАТЬ: И из n4810§20.11.3.6 Создание [util.smartptr.shared.create]

1 Общие требования , которые применяются ко всем make_shared, allocate_shared, make_shared_default_init, и allocate_shared_default_initперегрузках, если не указано иное, описаны ниже.

[...]

7 Примечания: (7.1) - Реализации должны выполнять не более одного выделения памяти. [Примечание: это обеспечивает эффективность, эквивалентную навязчивому интеллектуальному указателю. —Конечная записка]

[Акцент все мое]

Таким образом, стандарт говорит, что std::allocate_shared следует использовать Allocдля блока управления.

Пол Эванс
источник
1
Я извиняюсь по cppreference это не нормативный текст. Это отличный ресурс, но не обязательно для вопросов юристов .
StoryTeller - Unslander Моника
@ StoryTeller-UnslanderMonica Полностью согласен - просмотрел последний стандарт и не смог ничего найти, поэтому пошел с cppreference.
Пол Эванс
Нашел n4810и обновил ответ.
Пол Эванс
1
Однако речь идет make_sharedне о самих конструкторах. Тем не менее, я могу использовать член для небольших удалителей.
LF
3

Я считаю, что это не определено.

Вот спецификация соответствующих конструкторов: [util.smartptr.shared.const] / 10

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Эффекты. Создает shared_­ptrобъект, которому принадлежит объект pи средство удаления d. Когда Tтип не является массивом, первый и второй конструкторы включаются shared_­from_­thisс помощью p. Второй и четвертый конструкторы должны использовать копию aдля выделения памяти для внутреннего использования . Если выдается исключение, d(p)вызывается.

Теперь моя интерпретация заключается в том, что когда реализации требуется память для внутреннего использования, она делает это с помощью a. Это не значит, что реализация должна использовать эту память для размещения всего. Например, предположим, что есть эта странная реализация:

template <typename T>
class shared_ptr : /* ... */ {
    // ...
    std::aligned_storage<16> _Small_deleter;
    // ...
public:
    // ...
    template <class _D, class _A>
    shared_ptr(nullptr_t, _D __d, _A __a) // for example
        : _Allocator_base{__a}
    {
        if constexpr (sizeof(_D) <= 16)
            _Construct_at(&_Small_deleter, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

Использует ли эта реализация «копию aдля выделения памяти для внутреннего использования»? Да, это так. Он никогда не выделяет память, кроме как с помощью a. Есть много проблем с этой наивной реализацией, но давайте скажем, что она переключается на использование распределителей во всех случаях, кроме самого простого случая, в котором shared_ptrконструируется непосредственно из указателя и никогда не копируется, не перемещается и не ссылается иным образом, и других сложностей нет. Дело в том, что то, что мы не можем представить себе правильную реализацию, само по себе не доказывает, что она не может существовать теоретически. Я не говорю, что такая реализация действительно может быть найдена в реальном мире, просто что стандарт, похоже, не запрещает ее активно.

LF
источник
IMO ваш shared_ptrдля небольших типов выделяет память в стеке. И так не соответствует стандартным требованиям
бартоп
1
@bartop Он не «выделяет» какую-либо память в стеке. _Smaller_deleter безусловно является частью представления shared_ptr. Вызов конструктора в этом пространстве не означает выделения чего-либо. В противном случае даже указатель на блок управления считается «распределением памяти», верно? :-)
LF
Но средство удаления не обязательно должно быть копируемым, так как это будет работать?
Николь Болас
@NicolBolas Умм ... Используйте std::move(__d)и отступайте , allocateкогда требуется копирование.
LF