Part 6 - std::unique_ptr, std::shared_ptr and std::weak_ptr

You have no idea how long I've waited to write this part. In my opinion this is the single most important part in this entire series. Here, you will learn how to ditch new and delete forever.

Memory management in C++ is (almost) the same as in Rust. It's based on the concept of ownership. Each object on heap has only 1 owner, which is supposed to clean after it. To do this properly C++ uses std::unique_ptr.

The most important rule of C++ memory management - do not use owning raw pointers

That means, never use new and assign result to a raw pointer.

std::unique_ptr - single owner

To understand how std::unique_ptr works let's try to reinvent it. What if we had a class, that wrapped around raw pointers and called delete when no longer used?

template<T>
class unique_ptr
{
public:
// pseudocode here to show what I mean
unique_ptr(...args)
{
this->pointer = new T(...args);
};

~unique_ptr()
{
delete pointer;
};

// for now let's make pointer public
T* pointer;
};

The pifall here is that if we ever copy the object or just make another reference to it, everything will crash and burn:

// create new unique_ptr
auto a = unique_ptr<int>(3);

// sidenote - you can create a "scope" for variables anywhere by using {}
{
// create another unique_ptr
auto a = unique_ptr<int>(7);

auto b = a;

// a runs out of scope - destructor called
}

// oh no, we're accessing pointer after it got deleted!
b.pointer = 7;

To solve this pointer we delete copy constructor and copy assignment operator.

template<T>
class unique_ptr
{
public:
// ...rest of code here

// delete copy constructor
unique_ptr(const unique_ptr& other) = delete;

// delete copy assignment operator
unique_ptr operator=(const unique_ptr& other) = delete;

// ...rest of code here
};

Now if we try to do b = a compilation will fail. Ofc this is oversimplified, but you get the point. But what if we want to have a shared resource, that gets deleted after everyone stopped using it? That's where std::shared_ptr comes in.

std::shared_ptr - multiple owners

std::shared_ptr is a std::unique_ptr that has built-in reference counter, which is incremented every time you perform a copy and decremented on every destruction.

template<T>
class shared_ptr
{
public:
// pseudocode here to show what I mean
shared_ptr(...args)
{
// create resource
this->pointer = new T(...args);

// create counter
this->counter = new int(1);
};

~shared_ptr()
{
// decrement counter
--counter;

// if this was the last reference delete resource & counter
if (counter == 0)
{
delete pointer;
delete counter;
}
};

shared_ptr(const shared_ptr& other)
{
// copy stuff
this->pointer = other.pointer;
this->counter = other.counter;

// increment pointer
this->counter++;
}

shared_ptr operator=(const shared_ptr& other)
{
// copy stuff
this->pointer = other.pointer;
this->counter = other.counter;

// increment pointer
this->counter++;
}

// for now let's make pointer public
T* pointer;

// reference counter has to be a pointer to be shared across multiple instances
int* counter;
}

std::make_unique and std::make_shared

Default constructors of std::unique_ptr and std::shared_ptr take in a pointer (so you can cnvert regular raw pointer to a smart pointer, but you are responsible for stuff like guaranteeing that you didn't copy raw pointer before that). Usually you want to call std::make_unique or std::make_shared.

// a is std::unique_ptr
auto a = std::make_unique<int>(3);

// b is std::shared_ptr
auto b = std::make_shared<int>(4);

std::weak_ptr - cyclic references escape

But what if we have 2 objects, each wanting a std::shared_ptr to the other one? This would actually create what we call a cyclic refenrece - neither of the objects will destroy, because the other one is holding a std::shared_ptr to it. To avoid that we use `std::weak_ptr.

std::weak_ptr<int> wp;

void observe()
{
std::cout << "use_count == " << wp.use_count() << ": ";
if (auto spt = w.lock())
std::cout << *spt << "\n";
else
std::cout << "gw is expired\n";
}

{
auto sp = std::make_shared<int>(42);
// std::weak_ptr is constructed from std::shared_ptr
wp = sp;

// prints 42
observe();
}

// prints expired
observe();

actually using them

To dereference a smart pointer you can use * operator (just like you would do with normal pointer). To get the raw pointer out (be sure NOT to copy it anywhere) call get() method.

auto function qwe(const int& arg);
auto function xyz(const int* arg);

auto a = std::make_unique<int>(3);
qwe(*a);
xyz(a.get());
Mastodon