Smart pointers: auto_ptr, unique_ptr, shared_ptr, weak_ptr and raw pointers

23 January 2023

Since c++11 we have a lot of smart pointers, we see here the various types of pointers that exists out there, sorted by decreasing order of usefulness (according to me): - std::unique_ptr (since c++11) - raw pointer - std::shared_ptr (since c++11) - std::weak_ptr (since c++11) - std::auto_ptr (since c++98, deprecated in c++11, removed in c++17)

Contents

Why smart pointers

Take a look at this code:

class Foo {
  private:
    int bar;
};

void fun() {
  // dynamically creating object of class Foo
  Foo* p = new Foo();
}

int main() {
  for (int i = 0; i < 10; ++i) {
    fun();
  }
  return 0;
}

Function fun() creates a pointer to the Foo object, when fun() ends p will be destroyed as it is a local variable, but, the memory it consumed won't be deallocated because we forgot to use delete p at the end of the function. This is clearly a memory leak, and that's no good.

But this is just a simple case, we could just add a delete, but this can become very messy if, for example, we are using exceptions in our code. Look at this new fun() and main() functions:

void fun() {
  Foo* p = new Foo();
  if (/* some condition */ true) {
    throw std::runtime_error("fatal error");
  }
  delete p;
}

int main() {
  try {
    for (int i = 0; i < 10; ++i) {
      fun();
    }
  } catch (...) {
    return 1;
  }
  return 0;
}

Now we have a delete, problem solved, but what if an exception is thrown inside fun(), we will not reach the delete on the next line, and again, we will have a memory leak, and that's no good.

But what if the pointer deletes itself when out of scope? Local variables are deleted when exiting the scope they are in, and pointers are deleted too, but keep in mind a pointer stores a number, that references a memory position, so that number is what is deleted when existing the scope. Smart pointers provides a solution for this.

What is a smart pointer

Run the following code:

#include <iostream>

class Foo {
  public:
    Foo() {
      std::cout << "constructor\n";
    }
    ~Foo() {
      std::cout << "destructor\n";
    }
};

void bar() {
  std::cout << "Entering function...\n";
  Foo var();
  std::cout << "Exiting function...\n";
}

int main() {
  bar();
  return 0;
}

Output:

Entering function...
constructor
Exiting function...
destructor

As you can see, Foo() destructor is called when the whe exit the scope, but Foo var is not a pointer, this is just to see that variables get destroyed when out of scope (but as I said before, a pointer will not delete the element is pointed to, it will be just the pointer value, a memory address, what gets destroyed.

A smart pointer is a wrapper class over a pointer with a destructor and overloaded operators like * and ->. Since the destructor is automatically called when an object goes out of scope, the dynamically allocated memory would automatically be deleted (or reference count decremented), by a delete in the smart pointer destructor.

A small implementation of a smart pointer could look like this:

#include <iostream>

class SmartPtr {
  int* ptr; // actual pointer
public:
  // Constructor
  explicit SmartPtr(int* p = NULL) {
    ptr = p;
  }

  // Destructor
  ~SmartPtr() { delete (ptr); }

  // Overloading dereferencing operator
  int& operator*() {
    return *ptr;
  }
};

int main() {
  SmartPtr p(new int());
  *p = 20;
  cout << *ptr << "\n";

  // We don't need to call `delete p`, when the object p goes out of scope
  // the destructor for it is automatically called and the destructor
  // does `delete ptr`, so, no leaks, that's good.

  return 0;
}

Types of smart pointers

std::unique_ptr

since c++11

The semantics of std::unique_ptr is that it is the sole owner of a memory resource. A std::unique_ptr will hold a pointer and delete it in its destructor (unless you customize this passing another function to the template parameters of std::unique_ptr).

This also allows you to express your intentions on keeping that pointer in the scope in an interface. Look at the following code:

std::unique_ptr<Foo> bar();

It tells you that it gives a pointer to a Foo object, of which you are the owner. No one else will delete/free this pointer except the unique_ptr destructor when unique_ptr goes out of scope.

Since you get the ownership, this gives you confidence that you are free to modify the value of the pointed to object, and you can exit the scope at any point without worrying of delete/free the pointer (because the destructor will do it).

This works the other way around too, by passing an std::unique_ptr as a parameter (this also limites the std::unique_ptr to the function scope):

class Foo {
  public:
    Foo(std::unique_ptr<Bar> baz);
  // ...
};

In this case, Foo takes ownership of the Bar pointer.

Note though that even when you receive a std::unique_ptr, you are not guaranteed that no one else has access to the pointer. Indeed, if another context keeps a copy of the pointer inside you unique_ptr, then modifying the pointed to object through the unique_ptr object will of course impact this other context, and when one of them goes out of scope, it will delete/free the object! that's why unique_ptr doesn't have a copy constructor or an assign operator, it's not mean to have multiple instances, that is only bound to couse trouble.

But if for some reason you have multiple instances of the unique_ptr, and you want avoid modification of the pointed element, you can do it by using a unique_ptr to const:

// for some reason I don't want you to modify the `Foo` you are being passed
std::unique_ptr<const Foo> bar();

As I said before, to ensure that there is only one unique_ptr that owns a memory resource, std::unique_ptr cannot be copied. But, the ownership can however be transfered from ane unique_ptr to another (which is how you can pass them or return them from a function) by moving a unique_ptr into another one! And now the above example of the const pointed element makes sense!

A move can be achieved by returning an std::unique_ptr by value from a function, by passing one as argument, or explicitly in code using the move operator!

// create a unique_ptr
std::unique_ptr p1 = std::make_unique(42);
// move resource to p2
std::unique_ptr p2 = std::move(p1);
// now p2 holds the resource and p1 no longer holds anything

raw pointers

since always

Even if raw pointers are not smart pointers, they aren't "dumb" either. In fact there are legitimate reasons to use them although these reasons don't happen often. They share a lot with references, but the latter should be preferred whenever is possible, except in some cases.

Raw pointers and references represent access to an object, but not ownership.

In fact, this is the default way you should use to pass objects to functions and methods:

void foo(const Bar& bar);

This is porticularly relevant to note when you hold an object with a unique_ptr and want to pass it to an interface. You don't pass the unique_ptr, nor a reference to it, but rather a reference to the pointed object:

std::unique_ptr<Foo> foo = createFoo();
printFoo(*foo); // a reference to the pointed object

std::shared_ptr

since c++11

A single memory resource can be held by several std::shared_ptr at the same time.

The shared_ptrs internally maintein a count of how mony of them there are holding the same resource, and when the last one is destroyed (when it goes out of scope), it deletes/frees the memory resource.

Therefore std::shared_ptr allows copies, but with a reference-counting mechanism to make sure that every resource is deleted once and only once.

That makes std::shared_ptr an alternative for std::unique_ptr when we are gonna have more than one pointer to the same object, and we don't want to delete/free it when the one of this unique_ptr goes out of scope, so never use raw pointers again for this!

At first glance std::shared_ptr looks like the panacea for memory management, as it can be passed around and still maintein memory safety. But std::shared_ptr should not be used by defualt for several reasons: - Having several simultaneous holders for a resource makes for a more complex system than with one unique holder, like with std::unique_ptr. Even though an std::unique_ptr doesn’t prevent from accessing and modifying its resource, it sends a message that it is the priviledged owner of a resource. For this reason you’d expect it to centralize the control of the resource, at least to some degree. - Having several simultaneous holders of a resource makes thread-safety harder. - It makes the code counter-intuitive when an object in not shared in terms of the domain and still appears as "shared" in the code for a technical reason. - It can incur a performance cost, both in time and memory, because of the bookkeeping related to the reference-counting.

One good case for using std::shared_ptr though is when objects are shared in the domain. Using shared pointers then reflects it in a expressive way. Typically, the nodes of a graphs are well represented as shared pointers, because several nodes can hold a reference to ane other node.

std::weak_ptr

since c++11

std::weak_ptr can hold a reference to a shared object along with std::shared_ptr, but they don't increment the reference count.

This means that if no more std::shared_ptr are holding an object, this object will be deleted even if some weak pointers still point to it.

For this reasons, a weak pointer needs to chech if the object it points to is still alive. To do this, it has to be copied into a std::shared_ptr:

void foo(std::weak_ptr<int> wp) {
  if (std::shared_ptr<int> sp = wp.lock()) {
    // the resource is still alive
  } else {
    // the resource is no longer alive
  }
}

A typical use for this in about breaking shared_ptr circular references. Consider the following code:

struct Foo {
    std::shared_ptr<Foo> foo;
};

std::shared_ptr<Foo> p1 = std::make_shared<Foo>();
std::shared_ptr<Foo> p2 = std::make_shared<Foo>();;
p1->foo = p2;
p2->foo = p1;

None of the houses ends up being destroyed at the end of this code, because the shared_ptr points into one another. But if one is a weak_ptr instead, there is no longer a circular reference.

std::auto_ptr

since c++98, deprecated in c++11, removed in c++17

It aimed at filling the same need as unique_ptr, but back when move semantics didn't exist in C++.

It essentially does in its copy constructor what unique_ptr does in its move constructor, for that reasons is inferior to unique_ptr and you shouldn't use it if you have access to unique_ptr, because it can lead to erroneous code:

std::auto_ptr<int> p1(new int(42));
std::auto_ptr<int> p2 = p1;
// it looks like p1 == p2, but no, p1 is empty and p2 uses the resource

So please avoid using it.

Links

Sources