Power of Smart Pointers


Smart Pointers
Gone are the days where you bang your head against the table trying to figure out what went wrong with your code. If you had dealt with pointers in C++ before, you probably know what I mean. As powerful of a language C++ is, it is also error prone since you need to manage pointers and memory yourself. C++ 11 introduces a solution to this headache with smart pointers.

Overview

In this post, I will cover the following topics:

  • What is a smart pointer
  • Custom strc class
  • The different types of smart pointers and how to use each smart pointer with some code example
  • Using a custom deleter for smart pointer
  • Choosing between which smart pointer to use

I am going to assume you have a solid foundation with C++ for the purpose of this post.

What is a smart pointer?

A smart pointer is a template class that uses operator overloads to provide the functionality of a pointer while providing additional features to support memory management and safety. Another way to look at it is that a smart pointer is a wrapper around the standard bare c-language pointer with self-memory management that always points to a valid object. A smart pointer is designed to provide these additional benefits while operating as closely as possible to a standard pointer. To use smart pointers you just have to add the line “#include <memory>” into your code.

To get a better idea of why you want to use smart pointers, consider this case:

T * function();

We know this function returns a pointer, but what would you do with this pointer after you are done? Is the pointer static or allocated? Should we free the memory or let another process handle it? Should you use delete() or free()? All of these questions are unanswered and can easily lead to memory leaks.

Now consider this case again, but with smart pointer (unique pointer):

unique_ptr<T> function();

In this case, we know exactly what to do. Since this is a unique pointer, there is only one copy of this pointer. We know it cannot be copied because it is unique. You know you can safely destroy its associated resources once you are done.

Here is the case again, but with a shared pointer:

shared_pointer<T> function();

Since the function is returning a shared pointer you automatically know certain things. You can make copies of the pointer and delete those copies as much as you want. The resources will automatically and safely be destroyed when all copies of this shared pointer are deleted or go out of scope. The important takeaway is that the shared pointer manages the resource so you do not have to worry about it.

Custom strc class

The custom strc class is what I am going to use for all the sample code to show how each type of smart pointer works. The strc class prints out to console the order of events that happens for each type of smart pointer. Here is the strc class code for reference:

strc.h

#ifndef __STRC_H_
#define __STRC_H_

#include <cstddef>

static const char * strc_class = "strc";

class strc {
    static const size_t _maxlen = 10240;
    char * data = nullptr;
    void msg(const char *);
public:
    strc();
    strc(const char * s);
    strc(const strc &);
    strc(strc &&);
    ~strc();
    strc & operator = (strc);
    const char * value() const;
    operator const char * () const;
    void swap(strc &);
};

#endif // __STRC_H_

strc.cpp

#include "strc.h"
#include <cstdio>
#include <cstring>
#include <memory>

void strc::msg(const char * s) {
    if(data) printf("%s: %s (%s)\n", strc_class, s, data);
    else printf("%s: %s\n", strc_class, s);
    fflush(stdout);
}

strc::strc() : data(nullptr) {
    msg("default constructor");
}

strc::strc(const char * s) {
    size_t slen = strnlen(s, _maxlen);
    data = new char[slen + 1];
    data[slen] = 0;
    memcpy(data, s, slen);
    msg("cstring constructor");
}

strc::strc(const strc & f) {
    size_t slen = strnlen(f.data, _maxlen);
    data = new char[slen + 1];
    data[slen] = 0;
    memcpy(data, f.data, slen);
    msg("copy constructor");
}

strc::strc(strc && o) {
    data = std::move(o.data);
    o.data = nullptr;
    msg("move constructor");
}

strc::~strc() {
    msg("destructor");
    delete[] data;
}

strc & strc::operator = (strc o) {
    swap(o);
    msg("copy and swap =");
    return *this;
}

const char * strc::value() const {
    return data;
}

strc::operator const char * () const {
    return value();
}

void strc::swap(strc & o) {
    std::swap(this->data, o.data);
}

Unique Pointer

A unique pointer is a type of smart pointer that cannot be copied. There is only ever one copy of this pointer, so you cannot make copies of it and there is no ambiguity about who owns it. When the unique pointer gets destroy, it automatically calls the resource’s destructor.

Here are some important functions to a unique pointer and what they do:

  • reset() calls the destructor and allow unique pointer for reuse
  • move() can allow change of ownership and must be used if you want to let another variable be that unique pointer
  • release() do the same thing as reset() except it does not allow for reuse

Beginning with C++14 there is a “make_unique” template function to create a unique pointer for you, but that is not present in C++11. If you are using C++11 you can use the template function below to mimic the “make_unique” function from C++14.

namespace notstd {
    template <typename T, typename P>
    std::unique_ptr<T> make_unique(P initializer) {
       return std::unique_ptr<T> (new T(initializer));
    }
}

Code example and explanation

Here is the full code solution

unique_ptr.cpp

#include <cstdio>
#include <memory>
#include "strc.h"

void message(const char * s) {
    printf("\n%s\n", s);
    fflush(stdout);
}

// display object value from unique_ptr
void disp(std::unique_ptr<strc> & o) {
    if(o) puts(o->value());
    else puts("null");
    fflush(stdout);
}

namespace notstd {
    template <typename T, typename P>
    std::unique_ptr<T> make_unique(P initializer) {
        return std::unique_ptr<T>(new T(initializer));
    }
}

int main(int argc, const char ** argv) {

    message("create unique pointer one");
    std::unique_ptr<strc> a(new strc("one"));
    disp(a);

    message("make_unique two");
    auto b = notstd::make_unique<strc>("two");
    disp(a);
    disp(b);
 
    message("reset a to three");
    a.reset(new strc("three"));
    disp(a);
    disp(b);

    message("move b to c");
    auto c = std::move(b);
    disp(a);
    disp(b);
    disp(c);

    message("reset a");
    a.reset();
    disp(a);
    disp(b);
    disp(c);

    message("reset c");
    c.reset();
    disp(a);
    disp(b);
    disp(c);
 
    message("end of scope");
    return 0;
}

The following will be snippet code and its outputs to the terminal follow by an explanation.

    message("create unique pointer one");
    std::unique_ptr<strc> a(new strc("one"));
    disp(a);

Output

create unique pointer one
strc: cstring constructor (one)
one

The code creates a unique pointer of type strc with the value “one”. This invokes the cstring constructor of our strc class. After creating the unique pointer of type strc, the data value of the unique pointer gets output to the terminal.

    message("make_unique two");
    auto b = notstd::make_unique<strc>("two");
    disp(a);
    disp(b);

Output

make_unique two
strc: cstring constructor (two)
one
two

With this bit of code, we create a new strc unique pointer with the value “two”. The cstring constructor also handles the creation in this case. Notice this time we are using the template function “make_unique” to create the unique pointer. After creating the new unique pointer, we print out the value each strc unique pointer holds.

    message("reset a to three");
    a.reset(new strc("three"));
    disp(a);
    disp(b);

Output

reset a to three
strc: cstring constructor (three)
strc: destructor (one)
three
two

Here we reuse the unique pointer by using the “reset()” function. A lot actually happens in the line of code where reset() was called.

  1. The cstring constructor for strc was invoked to create a new strc of value “three”
  2. The destructor for strc was called
  3. The unique pointer changes to point at the new strc

Afterward, we print out the values of each unique pointer and what you notice is the value is now three instead of one.

    message("move b to c");
    auto c = std::move(b);
    disp(a);
    disp(b);
    disp(c);

Output

move b to c
three
null
two

This snippet of code shows how the move() function works. Notice that move() actually transfers ownership of the object (strc) that the original unique pointer points to. There is no copy of the unique pointer afterward as you can see the unique pointer now has a null value.

    message("reset a");
    a.reset();
    disp(a);
    disp(b);
    disp(c);

Output

reset a
strc: destructor (three)
null
null
two

Here we are just deleting the strc object by calling reset. Notice that if we do not pass in a new object into the reset() function only the destructor gets invoke. After the reset() function returns, the unique pointer has a value of null.

    message("reset c");
    c.reset();
    disp(a);
    disp(b);
    disp(c);

Output

reset c
strc: destructor (two)
null
null
null

Here we are deleting the strc object by calling reset. Notice that if we do not pass in a new object into the reset() function only the destructor gets invoke. After the reset() function returns, the unique pointer has a value of null.

Unique pointers are very powerful and are a valuable tool for managing memory and reducing the likelihood of memory leaks. The only constraint is that there can only be one copy of a specific unique pointer at any one time. If you find yourself needing to create copies of a smart pointer then you need to use a shared pointer.

Shared Pointers

There are two different ways to initialize a shared pointer. You can use the “make_shared” template function or explicitly declare a shared pointer. The code example will cover both ways to declare a shared pointer.

A shared pointer has a reference counter that keeps track of how many copies of the pointer is around. Every time the pointer gets copied the reference count increases by one. For every destroy, the reference count decreases by one. When the reference count is zero, the shared pointer invokes the object’s destructor. When a shared pointer goes out of scope, the destructor gets call automatically.

Code example and explanation

Here is the full code solution

shared_ptr.cpp

#include <cstdio>
#include <memory>
#include "strc.h"

void message(const char * s) {
    printf("\n%s\n", s);
    fflush(stdout);
}

// display object value from shared_ptr (with reference count)
void disp(const std::shared_ptr<strc> & o) {
    if(o) printf("%s (%ld)\n", o->value(), o.use_count());
    else puts("[null]");
    fflush(stdout);
}

int main(int argc, const char ** argv) {

    message("create pointer with new");
    auto a = std::make_shared<strc>("new");

    message("reset a to one");
    a.reset(new strc("one"));
    disp(a);

    message("b = a");
    auto b = a;
    disp(a);
    disp(b);
 
    printf("a == b %s\n", a == b ? "true" : "false");
    printf("*a == *b %s\n", *a == *b ? "true" : "false");
 
    message("reset a to two");
    a.reset(new strc("two"));
    disp(a);
    disp(b);

    printf("a == b %s\n", a == b ? "true" : "false");
    printf("*a == *b %s\n", *a == *b ? "true" : "false");

    message("b.swap(a)");
    b.swap(a);
    disp(a);
    disp(b);
 
    message("std::swap");
    std::swap(*a, *b);
    disp(a);
    disp(b);
 
    message("end of scope");
    return 0;
}
    message("create pointer with new");
    auto a = std::make_shared<strc>("new");

Output

create pointer with new
strc: cstring constructor (new)

This snippet of code creates a shared pointer with the template function make_shared. The object the shared pointer points to is a strc object with the value “new”.

    message("reset a to one");
    a.reset(new strc("one"));
    disp(a);

Output

reset a to one
strc: cstring constructor (one)
strc: destructor (new)
one (1)

Here we are using the reset() function to invoke the destructor. The strc cstring constructor gets invoke and creates a new strc object. The old strc object gets destroy and then the shared pointer now points to the new strc object. Then we display the value of the object and the reference count to that object.

    message("b = a");
    auto b = a;
    disp(a);
    disp(b);
 
    printf("a == b %s\n", a == b ? "true" : "false");
    printf("*a == *b %s\n", *a == *b ? "true" : "false");

Output

b = a
one (2)
one (2)
a == b true
*a == *b true

This snippet of code shows how copying a shared pointer works. The equal operator is overloaded to make creating a copy very simple. Afterward, we print out the value of both shared pointer and what we see is that they have the same value. Notice that the reference count increased by one. The shared pointer and the object it points to are also the base from the equality checks.

    message("reset a to two");
    a.reset(new strc("two"));
    disp(a);
    disp(b);

    printf("a == b %s\n", a == b ? "true" : "false");
    printf("*a == *b %s\n", *a == *b ? "true" : "false");

Output

reset a to two
strc: cstring constructor (two)
two (1)
one (1)
a == b false
*a == *b false

Here we are calling the destructor on a and set it to point to a new strc object with the value “two”. After reset() function returns, the object each shared pointer points are different. The reference counter that was 2 is now 1. The equality tests also are false, which indicates the shared pointers are not the same and the object they point are also not the same.

    message("b.swap(a)");
    b.swap(a);
    disp(a);
    disp(b);

Output

b.swap(a)
one (1)
two (1)

Here we are showing the usage of the swap() function for a shared pointer. It is important to note that this is not the swap function in the standard library. After swap() function returns, the object the shared pointer points to are swapped.

    message("std::swap");
    std::swap(*a, *b);
    disp(a);
    disp(b);

Output

std::swap
strc: move constructor (one)
strc: move constructor (two)
strc: copy and swap = (two)
strc: destructor
strc: move constructor (one)
strc: copy and swap = (one)
strc: destructor
strc: destructor
two (1)
one (1)

Notice how different the standard library swap() is compare with the swap() for a shared pointer. The object’s constructor and destructor are invoked when swapping the pointers. The process also invokes the object’s equal operator, which invokes a copy and swap function. This is a lot of overhead to just swap two objects. When you need to perform a swap operation, you should use the swap associated with shared pointers.

message("end of scope");

Output

end of scope
strc: destructor (one)
strc: destructor (two)

This snippet of code shows how shared pointers manage memory and resources. When the shared pointers go out of scope, the destructor automatically gets invoke. This means the object each shared pointer was pointing to are destroyed and have the memory they were occupying freed up.

Shared pointers are more flexible than unique pointers. You can have multiple copies of shared pointers and you are still guaranteed to have safe resource management. However, there is a problem with shared pointer and that is circular references. To solve this you need to use a special case of shared pointers call weak pointers.

Weak Pointers

A weak pointer is a special case of a shared pointer. It does not get counted in shared pointer reference count. A weak pointer is useful for cases where you need a pointer, but do not need it to affect the lifetime of the resources it points to.

You can create a weak pointer from a shared pointer. Although a weak pointer is created from a shared pointer, you cannot access the value or the managed object from a weak pointer directly. Instead, you need to request a lock to get the shared pointer.

You are probably wondering when would you need to use weak pointers. One case that a weak pointer will be useful is to avoid circular references. Consider this example involving a manager and employee relation.

Manager and employee circular reference example

smart pointer

Shared pointer leading to circular reference

In this example, a manager object points to the employees that it manages. At the same time, the employees point back to the manager they report to. These objects keep each other alive since they refer to each other. With smart pointers (shared pointers), the manager could never be destroyed as long as employees exist. Similarly, the employees can never be destroyed as long as the manager exists. In this situation, you cannot destroy either of them without first destroying the other one.

smart pointer

Weak pointer solution to circular reference

The solution to this circular reference problem is to have one of the two objects use a weak pointer. In this case, the employee can be destroyed without first destroying the manager. By making this change, the circular reference is no longer present.

Code example

Here is a code example to show how to work with weak pointers:

weak_ptr.cpp

#include <cstdio>
#include <memory>
#include "strc.h"

void message(const char * s) {
    printf("\n%s\n", s);
    fflush(stdout);
}

// display object value from shared_ptr (with reference count)
void disp(const std::shared_ptr<strc> & o) {
    if(o) printf("%s (%ld)\n", o->value(), o.use_count());
    else puts("[null]");
    fflush(stdout);
}

// display object value from weak_ptr (with reference count)
void disp(const std::weak_ptr<strc> & o) {
    // must get a lock to use a weak_ptr
    size_t count = o.use_count(); // weak pointer count
    auto sp = o.lock();
    if(sp) printf("%s (w:%ld s:%ld)\n", sp->value(), count, sp.use_count());
    else puts("[null]");
    fflush(stdout);
}

int main(int argc, const char ** argv) {

    message("create shared_ptr");
    auto a = std::make_shared<strc>("thing");

    message("make several copies");
    auto c1 = a;
    auto c2 = a;
    auto c3 = a;
    auto c4 = a;
    auto c5 = a;

    message("reference count is now 6");
    disp(a);
 
    message("create weak_ptr");
    auto w1 = std::weak_ptr<strc>(a);
    disp(w1);

    message("destroy copies");
    c1.reset();
    c2.reset();
    c3.reset();
    c4.reset();
    c5.reset();

    message("reference count should be 1");
    disp(a);

    message("check weak pointer");
    disp(w1);

    message("destroy a");
    a.reset();

    message("check weak pointer");
    disp(w1);

    message("end of scope");
    return 0;
}

Code breakdown and explanation

message("create shared_ptr");
    auto a = std::make_shared<strc>("thing");

    message("make several copies");
    auto c1 = a;
    auto c2 = a;
    auto c3 = a;
    auto c4 = a;
    auto c5 = a;

    message("reference count is now 6");
    disp(a);

    message("create weak_ptr");
    auto w1 = std::weak_ptr<strc>(a);
    disp(w1);

Output

create shared_ptr
strc: cstring constructor (thing)

make several copies

reference count is now 6
thing (6)

create weak_ptr
thing (w:6 s:7)

Here we are creating six shared pointers with each one pointing to the same object. Notice the reference count is 6 before the creation of the weak pointer. After the weak pointer is created from the shared pointer, the value of the weak pointer gets output to the terminal. It is important to note that we can only get the use count from the weak pointer only. To access the object and it’s value, the weak pointer must request for a lock, which returns the shared pointer the weak pointer was created from.

    message("destroy copies");
    c1.reset();
    c2.reset();
    c3.reset();
    c4.reset();
    c5.reset();

    message("reference count should be 1");
    disp(a);

    message("check weak pointer");
    disp(w1);

Output

destroy copies

reference count should be 1
thing (1)

check weak pointer
thing (w:1 s:2)

This snippet of code resets most of the shared pointers leaving one left. Afterward, we confirm the shared pointers was indeed destroyed by printing the value and reference counter of the original shared pointer. Next, we check the weak pointer and what we see is that the weak pointer reference counter is same as the shared pointer. The reference count increases by one when the weak pointer requests a lock to access the object because lock() returns a copy of the shared pointer the weak pointer was created from.

    message("destroy a");
    a.reset();

    message("check weak pointer");
    disp(w1);

Output

destroy a
strc: destructor (thing)

check weak pointer
[null]

Here we see how a weak pointer behaves when all the shared pointers copies it was created from are gone. After the shared pointer got reset, the weak pointer was not able to successfully request for a lock to access the object. Notice that the shared pointer the weak pointer was created from was not kept alive with the weak pointer present.

Custom Deleter

Sometimes you may want to use a custom deleter in addition to the managed object’s destructor. This can allow you to do extra actions after a specific smart pointer is destroyed. When you are using a custom deleter, you cannot use the make_shared template function. There are three ways to create a smart pointer with a customer deleter. The custom deleter can be:

  1. custom deleter function
  2. custom class as a Functor
  3. lambda closure

Code example

Here is a code example of defining and using a customer deleter

#include <cstdio>
#include <memory>
#include "strc.h"

/**
 * Class Functor example
 */
class D {
    public:
        void operator()(const strc * o) {
            printf("deleter: ");
            if (o) printf("%s\n", o->value());
            else printf("[null]\n");

            fflush(stdout);
            delete o;
        }
};

void message(const char * s) {
    printf("\n%s\n", s);
    fflush(stdout);
}

// display object value from shared_ptr (with reference count)
void disp(const std::shared_ptr<strc> & o) {
    if(o) printf("%s (%ld)\n", o->value(), o.use_count());
    else puts("[null]");
    fflush(stdout);
}

void deleter(const strc * o) {
    printf("deleter: ");
    if(o) printf("%s\n", o->value());
    else printf("[null]\n");
   fflush(stdout);
    delete o;
}

int main(int argc, const char ** argv) {

 message("create shared_ptr");
    std::shared_ptr<strc> a(new strc("thing"), &deleter); // function
    //std::shared_ptr<strc> a(new strc("thing"), D());        // class functor
    //std::shared_ptr<strc> a(new strc("thing"), [=](strc * o) {delete o;}); // lambda closure
    disp(a);

    a.reset();
    disp(a);

    message("end of scope");
    return 0;
}

Code breakdown and explanation

    message("create shared_ptr");
    std::shared_ptr<strc> a(new strc("thing"), &deleter); // function
    //std::shared_ptr<strc> a(new strc("thing"), D());    // class functor
    //std::shared_ptr<strc> a(new strc("thing"), [=](strc * o) {delete o;}); // lambda closure
    disp(a);

    a.reset();
    disp(a);

    message("end of scope");
    return 0;

Output

create shared_ptr
strc: cstring constructor (thing)
thing (1)
strc: destructor (thing)
[null]

end of scope

The snippet of code shows how to define a shared pointer using a custom deleter in three different ways. All three ways have the same output. Notice that the object’s destructor gets invoke first then follows by the custom deleter.

Choosing a smart pointer

Each type of smart pointer has their own unique usage and it really depends on how you will be using them that determines which smart pointer you should use. However, that does not mean there is not a smart pointer that will meet your needs most of the time. The default smart pointer most developers use is a shared pointer. This is because it is powerful, flexible, safe, and works very similar to unmanaged pointers but still manages objects nicely.

You should use a weak pointer if you know you are going to run into circular references scenarios. Weak pointers are also useful for circumstances where you need an optional value. In another word, you just need to know if the pointer is valid or not. In addition, weak pointers are useful in cases where you need to link to a resource that is managed by another object.

Lastly, there are unique pointers. You would want to use a unique pointer for cases where you need to know that a resource is only available to one object at a time. A unique pointer operates in a one-to-one relationship model. It cannot be copied, but you can take an L reference of a unique pointer.

Conclusion

Regardless of which smart pointer you decide to use, you will immediately notice its impact on your code. Smart pointers will greatly reduce memory management and memory leak problems. Smart pointers also ensure safety by guaranteeing the managed object and its resources are valid.

I hope this post is helpful to some of you out there and will help you write more robust software. If you find this helpful, share it with others who can benefit from this post. I rarely do post this technical and fill with code, so please let me know if this structure was easy for you to follow. If not then maybe some suggestions I can apply to make it a better experience for you. Also, do not forget to follow me on twitter to find out when new content comes out. Happy coding!


About Steven To

Steven To is a software developer that specializes in mobile development with a background in computer engineering. Beyond his passion for software development, he also has an interest in Virtual Reality, Augmented Reality, Artificial Intelligence, Personal Development, and Personal Finance. If he is not writing software, then he is out learning something new.