Почему shared_ptr использует размещение нового

Я читал во многих местах, что при использовании make_shared<T> для создания shared_ptr<T> его управляющий блок содержит блок хранения, достаточно большой для хранения T, а затем объект создается внутри хранилища с помощью размещение нового. Что-то вроде этого:

template<typename T>
struct shared_ptr_control_block {
    std::atomic<long> count;
    std::atomic<long> weak_count;
    std::aligned_storage_t<sizeof (T), alignof (T)> storage;
};

Но я немного смущен, почему мы не можем просто иметь переменную-член с типом T вместо этого? Зачем создавать исходное хранилище, а затем использовать новое место размещения? Не может ли он быть объединен в один шаг с обычным объектом типа T?

Ответ 1

Это позволяет управлять жизненным циклом.

Контрольный блок не разрушается до тех пор, пока weak_count не будет равен нулю. Объект storage уничтожается, как только count достигает нуля. Это означает, что вам нужно напрямую вызвать деструктор storage, когда счетчик достигнет нуля, а не в деструкторе блока управления.

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

Если бы у нас был только сильный подсчет ссылок, тогда T был бы точным (и намного проще).


На самом деле реализация немного сложнее, чем эта. Помните, что shared_ptr можно построить, выделив T с помощью new, а затем создав из него shared_ptr. Таким образом, фактический блок управления больше похож:

template<typename T>
struct shared_ptr_control_block {
    std::atomic<long> count;
    std::atomic<long> weak_count;
    T* ptr;
};

и что make_shared выделяет:

template<typename T>
struct both {
    shared_ptr_control_block cb;
    std::aligned_storage_t<sizeof (T), alignof (T)> storage;
};

И cb.p установлен на адрес storage. Выделение структуры both в make_shared означает, что мы получаем одно распределение памяти, а не два (и распределения памяти дороги).

Примечание. Я упростил: для деструктора shared_ptr должен быть способ узнать, является ли блок управления частью both (в этом случае память не может быть освобождена до завершения) или нет (в этом случае он может быть освобожден ранее). Это может быть простой флаг bool (в этом случае блок управления больше) или с помощью некоторых запасных бит в указателе (который не переносится, но стандартная реализация библиотеки не должна быть переносимой). Реализация может быть еще более сложной, чтобы избежать хранения указателя вообще в случае make_shared.

Ответ 2

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

Тот факт, что хранилище остается выделенным даже после уничтожения самого объекта, фактически может быть недостатком make_shared в системах ограничения памяти (хотя я не знаю, было ли это что-то на практике).