Smart Pointers in C++
Tags: Cpp, C, Smart Pointer, C Sharp Programming, C Programming

A deep dive

Let’s use a very simple, self-explanatory class for the rest of the article:

class Entity {

private:

int entity_ = 0; // Default value initialized public:

Entity() {

std::cout << "Constructing entity!"

<< std::endl;

} Entity(const int& x) {

entity_ = x;

std::cout << "Constructing entity with an integer"

<< std::endl;

} // Creating a copy constructor

Entity(const Entity& entity) {

entity_ = entity.entity_;

} ~Entity() {

std::cout << "Destructing entity!"

<< std::endl;

} // Marked as const => can not assign data members here

void getEntity() const {

std::cout << "The entity is" << entity_

<< std::endl;

} // const reference => no parameter modified here

void setEntity(const int& ent) {

entity_ = ent;

}

};

There are three types of smart pointers in C++. Let’s look at when and how to use them.

There are three types of smart pointers in C++. Let’s look at when and how to use them.

I. std::unique_ptr

When reaching for a smart pointer, std::unique_ptr should be the closest at hand:

A std::unique_ptr takes up as much space as a raw pointer, but with certain added advantages. A std::unique_ptr incorporates exclusive ownership of the object it points to. When the std::unique_ptr is deleted and it is not empty, it automatically destroys the object it points to. std::unique_ptr can NOT be copied (if you look at its implementation, you’ll notice the copy constructor is deleted). This is intuitive, too — say a std::unique_ptr points to object “a” and we copy it to another std::unique_ptr; when the first one goes out of scope it deletes “a”, then what happens to the second std::unique_ptr? It can and should be moved though, using std::move. Attempting to assign a raw pointer to a std::unique_ptr will cause a compiler error and should be avoided. A std::unique_ptr should rather be created by using std::make_unique. std::unique_ptrs are small, fast and, as we will subsequently, can easily be converted to std::shared_ptrs.

Here is a code snippet to illustrate the above points:

#include <memory>

#include <Entity.h> // Beginning of a scope

int main()

{

std::unique_ptr<Entity> e1 = std::make_unique<Entity>(6);

e1->getEntity(); // unique_ptr is smart, it does not

// need to be deleted -- it deallocates

// as it goes out of scope // However, unique_ptr cannot be copied (it's copy

// constructor is deleted). So,

// std::unique_ptr<Entity> e2 = e1 does NOT work

std::unique_ptr<Entity> e2(std::move(e1)); // point to e1 // e1->getEntity() will fail because e1 has been freed

e2->getEntity();

} // End of main

As expected, the output when the above is run,

Constructing entity with an integer!

The entity is 6

The entity is 6

Destructing entity!

As the scope ends, e2 's destructor is automatically called.

II. std::shared_ptr

When we might expect more than one pointer to point to our object, we use a std::shared_ptr . A std::shared_ptr manages the lifetime of an object via shared ownership. It does so by maintaining a "reference count" that keeps track of how many shared pointers are actively pointing to the current object. The reference count is part of a control block that stores other information as well (like custom deleters, if any). When all the shared pointers go out of scope (i.e. the reference count reaches 0), the object they were pointing to is deleted.

Avoid creating a std::shared_ptr by using raw pointers. Assume you do the following:

auto e1 = new Entity;

std::shared_ptr<Entity> shared_e1(e1);

std::shared_ptr<Entity> shared_e2(e1);

What can possibly go wrong?

Now, shared_e1 has a control block pointing to e1 , and so does shared_e2 . Hence e1 has two reference counts. When shared_e1 goes out of scope all will be well, but then shared_e2 will try to free e1 (which has already been freed), which will throw an error.

2. Use std::make_shared instead

3. Compared to std::unique_ptr, std::shared_ptr are twice as big for they have the added overhead of control blocks

4. Copying is definitely allowed and so is moving.

Sample code:

#include <memory>

#include <Entity.h> // Outer scope

int main() {

std::shared_ptr<Entity> shared_e1 =

std::make_shared<Entity>(10); // Let’s create an empty scope

{

// Shared pointer keeps track of reference counts

std::shared_ptr<Entity> e2 = shared_e1; // This will alter shared_e1 as well, since

// both refer to the one address location of entity

e2->setEntity(2); shared_e1->getEntity();

e2->getEntity();

} // Destructor is called after this scope ends –

// only when BOTH go out of scope

std::cout << "Destructor NOT yet called"

<< std::endl;

} // End of main() // Destructor is called here

The output will be:

Constructing entity with an integer!

The entity is 2

The entity is 2

Destructor NOT yet called

Destructing entity!

III. std::weak_ptr

Weak pointers, unfortunately, are aptly named. Assume a kind of std::shared_ptr that can point to a shared object, but cannot update said object's reference count. That precisely is a weak pointer. Why would we need a std::weak_ptr , then?

We use std::weak_ptr to keep track of whether an object is alive or not - a std::weak_ptr can safely dangle! Hence a std::weak_ptr is obviously lighter than a std::shared_ptr When the reference count of the object goes to 0, it is not alive anymore, as will be returned by calling the function expired() on a weak pointer. If it is alive, we can’t really use a std::weak_ptr for any viable function. However, we can create a std::shared_ptr from it and use that for all purposes. We use the function lock() on the said std::weak_ptr .

Here is an example:

#include <memory>

#include <Entity.h> // Outer scope

int main()

{

std::shared_ptr<Entity> shared_e1

= std::make_shared<Entity>(14); // Empty scope

{

std::weak_ptr<Entity> weak = shared_e1; // Weak pointer does not increase the reference count

// We can NOT do weak->getEntity(); However: // Use of lock()

std::shared_ptr<Entity> new_shared = weak.lock(); // Weak does NOT dangle yet

shared_e1 = nullptr; // Weak now dangles and destructor is called

new_shared = nullptr; if (weak.expired()) {

std::cout << "My weak pointer is dangling :("

<< std::endl;

} // new_shared is now null; it is a

// bad practice, by the way

new_shared = weak.lock(); std::cout << new_shared << std::endl;

} // End of empty scope

} // End of main()

The output, not surprisingly, is: