The memory of an object managed by a std::shared_ptr created using std::make_shared won’t be freed until all associated std::shared_ptr and all std::weak_ptr instances are destroyed. However, if the object is created separately and then passed into a std::shared_ptr, its memory will be freed once all std::shared_ptr instances are destroyed—regardless of whether any associated std::weak_ptrs still exist.

Detailed description Link to heading

Suppose you have a relatively large object managed by a shared pointer:

class HeavyObjectStore {
  public:
    HeavyObjectStore()
      : object_{ std::make_shared<HeavyObject>() }
    {}

    std::shared_ptr<HeavyObject> getObject() const { return object_; }
  private:
    std::shared_ptr<HeavyObject> object_;
};

The HeavyObject is used in several places, each holding a copy of the std::shared_ptr. That means the lifetime of the HeavyObject is extended as needed:

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() };
}

There are some other places that use the HeavyObject, but in a different way. In these places we don’t want to extend the lifetime of HeavyObject, but rather use it if the object hasn’t been deleted yet. In this case we are sure that the memory occupied by a HeavyObject is released as soon as possible, when all places which strictly need the HeavyObject have already finished their work with it and destroyed their std::shared_ptrs.

To try to create a std::shared_ptr only if another std::shared_ptr still exists, we use the std::weak_ptr<HeavyObject>::lock() method:

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_;
};

Let’s also suppose that std::weak_ptr was used here because we want the HeavyObject to be destroyed and its memory freed as soon as possible—that is, when all existing std::shared_ptr instances have been destroyed.

You run the code and you see that the memory is not being freed when all std::shared_ptr instances are destroyed—but the memory is being freed when the last associated std::shared_ptr or std::weak_ptr instance is being destroyed. Why does this happen?

Let’s go through two main ways to create a std::shared_ptr.

  1. The first way is to allocate the object separately and use the std::shared_ptr<T>(T* obj) constructor to pass the pointer inside. In this case, the shared pointer only needs to allocate the control block.
  2. The second way is to use std::make_shared (or similar), which allocates both the object and the control block using only one allocation, i.e. in one memory chunk.

If both are possible, the second option is recommended, since a single allocation is typically faster than two. Also, in the case of separate allocations, you need to be extra careful with cases where the object’s memory is already allocated, a std::shared_ptr hasn’t yet been created, and an exception is thrown. This will lead to a memory leak.

A std::weak_ptr uses the same control block as a std::shared_ptr. In the control block, there is separate reference count for std::weak_ptr references, alongside counting of std::shared_ptr references. When all std::shared_ptrs are destroyed and their reference count drops to zero, the managed object is destroyed. However, if there are still std::weak_ptr instances that reference the same control block, the block itself won’t be destroyed or deallocated until all of std::weak_ptrs are destroyed.

The key point is that if the control block and the object were allocated in a single chunk, that chunk can only be deallocated as a whole. We can’t deallocate part of it, and later deallocate the other part. But, if the control block is allocated separately, then the object can be destroyed and its memory freed independently of the control block.

In other words:

  • If the std::shared_ptr was created using std::make_shared, then:
    • the managed object will be destroyed when the std::shared_ptrs reference count drops to zero;
    • the memory of the managed object won’t be freed, until there are instances of std::weak_ptr pointing to the control block.
  • If the std::shared_ptr was created by passing a pointer to an already existing object, then the object will be destroyed and its memory freed when the std::shared_ptrs reference count drops to zero, regardless of std::weak_ptrs presence.

We can check this with the following code:

#include <cstddef>
#include <iostream>
#include <memory>

template <typename T>
class Allocator {
 public:
  using value_type = T;

  Allocator() = default;

  template <typename U>
  explicit 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));
  }
};

struct Object {
  Object() { std::cout << "Object created.\n"; }

  ~Object() { std::cout << "Object destroyed.\n"; }

 private:
  [[maybe_unused]] char x[400];
};

std::shared_ptr<Object> CreateWithAllocateShared() {
  return std::allocate_shared<Object>(Allocator<Object>{});
}

std::shared_ptr<Object> CreateByPassingPointer() {
  Allocator<Object> allocator{};
  Object* const v = allocator.allocate(1);
  new (v) Object{};
  const auto deleter = [](Object* const ptr) {
    Allocator<Object>{}.deallocate(ptr, 1);
  };
  return {v, deleter, allocator};
}

int main() {
  {
    std::weak_ptr<Object> w;

    std::cout << "Creating shared_ptr.\n";
    {
      // std::shared_ptr<Object> p = CreateWithAllocateShared();
      // or
      std::shared_ptr<Object> p = CreateByPassingPointer();

      std::cout << "Creating weak_ptr.\n";
      w = p;
      std::cout << "Deleting shared_ptr.\n";
    }
    std::cout << "Deleted shared_ptr.\n"
              << "Deleting weak_ptr.\n";
  }
  std::cout << "Deleted weak_ptr.\n";
  std::cout << "End of function.\n";
}

We get the following output when we use 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.

And this output when the object is created separately:

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.

As we can see, in the first case, when using std::allocate_shared, the memory is deallocated all at once during the destruction of the std::weak_ptr. In the second case, when the object is allocated separately from the control block, the object’s memory is deallocated when the last std::shared_ptr is destroyed and the memory allocated for the control block is deallocated during the destruction of the std::weak_ptr.

The cppreference page on std::shared_ptr doesn’t mention this behavior:

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(). 

It’s not mentioned on the std::allocated_shared page either. However, it is noted on the std::make_shared page:

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.