Память объекта, которым владеет std::shared_ptr
, созданный с помощью std::make_shared
, не будет высвобождена до тех пор, пока не будут уничтожены все связанные std::shared_ptr
и std::weak_ptr
. Однако если объект создаётся отдельно и затем передаётся в std::shared_ptr
, его память будет высвобождена, как только все экземпляры std::shared_ptr
будут уничтожены — вне зависимости от того, существуют ли связанные std::weak_ptr
.
Подробное описание Ссылка на заголовок
Предположим, что у нас есть достаточно крупный объект, которым владеет std::shared_ptr
:
class HeavyObjectStore {
public:
HeavyObjectStore()
: object_{ std::make_shared<HeavyObject>() }
{}
std::shared_ptr<HeavyObject> getObject() const { return object_; }
private:
std::shared_ptr<HeavyObject> object_;
};
HeavyObject
используется в нескольких местах, в каждом из которых есть копия std::shared_ptr
. Это означает, что время жизни HeavyObject
будет продлено в местах использования по мере необходимости:
class AnotherUserOfHeavyObject {
public:
explicit AnotherUserOfHeavyObject(std::shared_ptr<HeavyObject> s)
: object_{std::move(s)} {}
private:
std::shared_ptr<HeavyObject> object_;
};
...
AnotherUserOfHeavyObject CreateUser(const HeavyObjectStore& store) {
return { store.getObject() };
}
Есть ещё и несколько другое использование HeavyObject
:
class MaybeUserOfHeavyObject {
public:
MaybeUserOfHeavyObject(std::weak_ptr<HeavyObject> object)
: object_{std::move(object)} {}
void MaybeUseObject() {
if(std::shared_ptr<HeavyObject> s = object_.lock()) {
s->DoSomeStuff();
}
}
private:
std::weak_ptr<HeavyObject> object_;
};
Здесь время жизни HeavyObject
не продлевается ещё одной копией std::shared_ptr
. Вместо этого HeavyObject
используется только в том случае, если есть ещё хотя бы один std::shared_ptr
, владеющий данным объектом. Для этого используется метод std::weak_ptr<HeavyObject>::lock()
. Предположим также, что std::weak_ptr
используется здесь потому, что мы хотим рационально использовать память и, следовательно, хотим, чтобы HeavyObject
был удалён и используемая им память была освобождена, как только все std::shared_ptr
будут уничтожены.
Однако после запуска кода мы наблюдаем следующую картину: память, выделенная под объект HeavyObject
, не освобождается сразу после удаления всех std::shared_ptr
. После отладки становится понятно, что память освобождается после удаления всех оставшихся std::weak_ptr
. Почему так происходит?
Рассмотрим два основных способа создать std::shared_ptr
.
- Первый способ — создать объект отдельно и использовать конструктор
std::shared_ptr<T>(T* obj)
, чтобы передать внутрь указатель на этот объект. В этом случае shared pointer должен выделить только память для контрольного блока. - Второй способ — использовать
std::make_shared
(или подобные), которые выделяют и объект, и контрольный блок одной аллокацией, то есть в одном блоке памяти.
Если возможны оба способа, то рекомендуется второй, поскольку одна аллокация обычно быстрее двух. Кроме того, при использовании “голых” указателей следует быть особенно осторожным, чтобы не допустить утечек памяти, которые могут произойти, если после выделения памяти под объект, но до передачи данного указателя в std::shared_ptr
, будет выброшено исключение.
В контрольном блоке ведётся подсчёт ссылок std::shared_ptr
и (отдельно) ссылок std::weak_ptr
. Когда все std::shared_ptr
уничтожены, соответствующий счётчик ссылок становится равен нулю и управляемый объект уничтожается (вызов деструктора). Однако если всё ещё существуют ссылающиеся на тот же контрольный блок экземпляры std::weak_ptr
, то сам блок не будет уничтожен и освобождён до тех пор, пока не будут уничтожены все std::weak_ptr
, ведь для их работы контрольный блок всё ещё необходим.
Ключевой момент в том, что если контрольный блок и объект были выделены одной аллокацией в одном блоке памяти, то эта память может быть освобождена только полностью. Мы не можем освободить сначала одну часть блока, а затем другую. Но если контрольный блок был выделен отдельно, то объект может быть уничтожен, и его память может быть освобождена независимо от контрольного блока.
Иными словами:
- Если
std::shared_ptr
был создан с помощьюstd::make_shared
, то:- деструктор объекта, которым владеет
std::shared_ptr
, будет вызван, когда счётчик ссылокstd::shared_ptr
станет равен нулю; - память, где находится управляемый объект, не будет освобождена, пока существуют экземпляры
std::weak_ptr
, указывающие на контрольный блок.
- деструктор объекта, которым владеет
- Если
std::shared_ptr
был создан путём передачи указателя на уже существующий объект, то объект будет уничтожен и его память освобождена, когда счётчикstd::shared_ptr
станет равен нулю, независимо от наличияstd::weak_ptr
.
Мы можем проверить это следующим образом:
#include <iostream>
#include <memory>
template <typename T>
class Allocator {
public:
using value_type = T;
Allocator() = default;
template <typename U>
Allocator(const Allocator<U>&) {}
T* allocate(const std::size_t n) {
std::cout << "Allocating " << n * sizeof(T) << " bytes.\n";
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* const p, const std::size_t n) {
std::cout << "Deallocating " << n * sizeof(T) << " bytes.\n";
::operator delete(static_cast<void*>(p));
}
};
template <typename T>
class AllocatorDeleter {
public:
void operator()(T* ptr) {
Allocator<T> allocator;
allocator.deallocate(ptr, 1);
}
};
struct Value {
Value() { std::cout << "Value is created.\n"; }
~Value() { std::cout << "Value is destroyed.\n"; }
private:
char x[400];
};
std::shared_ptr<Value> CreateWithAllocateShared() {
return std::allocate_shared<Value>(Allocator<Value>{});
}
std::shared_ptr<Value> CreateByPassingPointer() {
Allocator<Value> allocator{};
Value* v = allocator.allocate(1);
new (v) Value{};
return {v, AllocatorDeleter<Value>{}, allocator};
}
int main() {
{
std::weak_ptr<Value> w;
std::cout << "Creating shared_ptr.\n";
{
std::shared_ptr<Value> p = CreateWithAllocateShared();
// или
// std::shared_ptr<Value> p = CreateByPassingPointer();
std::cout << "Creating weak_ptr.\n";
w = p;
std::cout << "Deleting shared_ptr.\n";
}
std::cout << "shared_ptr was deleted.\n"
<< "Deleting weak_ptr.\n";
}
std::cout << "weak_ptr was deleted.\n";
std::cout << "End of function.\n";
}
Следующий вывод получается при использовании std::allocate_shared
(Clang/GCC/MSVC):
Creating shared_ptr.
Allocating 416 bytes.
Object created.
Creating weak_ptr.
Deleting shared_ptr.
Object destroyed.
Deleted shared_ptr.
Deleting weak_ptr.
Deallocating 416 bytes.
Deleted weak_ptr.
End of function.
А такой вывод, при отдельном создании объекта:
Creating shared_ptr.
Allocating 400 bytes.
Object created.
Allocating 24 bytes.
Creating weak_ptr.
Deleting shared_ptr.
Deallocating 400 bytes.
Deleted shared_ptr.
Deleting weak_ptr.
Deallocating 24 bytes.
Deleted weak_ptr.
End of function.
Здесь видно, что в первом случае, при использовании std::allocate_shared
, память высвобождается во время уничтожения std::weak_ptr
. Во втором случае, когда объект выделяется отдельно от контрольного блока, память объекта высвобождается при уничтожении последнего std::shared_ptr
, а память, которая была выделена под контрольный блок, высвобождается во время уничтожения последнего std::weak_ptr
.
На странице cppreference о std::shared_ptr
это поведение не упоминается:
The object is destroyed and its memory deallocated when either of the following happens:
the last remaining shared_ptr owning the object is destroyed;
the last remaining shared_ptr owning the object is assigned another pointer via operator= or reset().
Не упомянуто также на странице std::allocate_shared
, но указано на странице std::make_shared
:
If any std::weak_ptr references the control block created by std::make_shared after the lifetime of all shared owners ended, the memory occupied by T persists until all weak owners get destroyed as well, which may be undesirable if sizeof(T) is large.