C++: Smart pointers, part 4: shared_ptr

This is the fourth post of several posts I wrote related to smart pointers:

  1. Smart pointers
  2. unique_ptr
  3. More on unique_ptr
  4. shared_ptr
  5. weak_ptr

As I mentioned in other posts, C++11 brings a new set of smart pointers into C++. The most useful smart pointer is shared_ptr: Its memory management policy consists in counting the number of shared_ptr instances that refer to the same object in the heap.

For example, if you have something like this:

shared_ptr<int> x(new int { 6 });
shared_ptr<int> y = x;

Your smart pointers x and y refer to the same integer created in the heap and both store the number of smart pointers that point to the same object (in our case, 2).

If we add something like:

x = nullptr;

The number of smart pointers referring to the same object is decremented to 1 because only y is referring it. When y gets out of scope, the shared_ptr destructor is invoked automatically (because of RAII) and the reference counter is decremented again (this time to 0, because no shared_ptr is pointing anymore to the object). Since the reference counter is 0, the object (in this case, our int { 6 }) destructor is invoked.

shared_ptr solves a lot of memory management problems and render the naked pointers (e.g. DataType*) unnecessary.

Consider this example in C++03:

#include <cstdio>
#include <cstring>

class Integer
{
	int n;
	public:
	  Integer(int n) : n(n) { }
	  ~Integer() { printf("Deleting %d\n", n); }
	  int get() const { return n; }
};

int main()
{
	Integer* a = new Integer(10);
	Integer* b = new Integer(20);
	Integer* c = a;
	Integer* d = new Integer(30);
	Integer* e = b;
	a = d;
	b = new Integer(40);
	Integer* f = c;
	b = f;
	
	printf("%d\n", a->get());
	printf("%d\n", b->get());
	printf("%d\n", c->get());
	printf("%d\n", d->get());
	printf("%d\n", e->get());
	printf("%d\n", f->get());
	
	delete a;
	delete b;
	delete c;
	delete e;
	delete f;
}

When it runs, it returns something like:


30
10
10
30
20
10
Deleting 30
Deleting 10
Deleting 10
v(663) malloc: *** error for object 0x7fb4024000e0: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

The program contains two severe problems: Memory leaks and crashes. If you look at the result, the Integer(40) instance has disappeared and its destructor has never been invoked, turning it into a memory leak. Also, we created 4 instances of the Integer class and we are invoking delete with 6 variables; what occurs right there is that one object is being deleted twice, producing a crash the second time.

shared_ptr comes to the rescue in C++11 and turns our code into something like this:

#include <cstdio>
#include <cstring>

#include <memory>

using namespace std;

class Integer
{
	int n;
	public:
	  Integer(int n) : n(n) { }
	  ~Integer() { printf("Deleting %d\n", n); }
	  int get() const { return n; }
};

int main()
{
	shared_ptr<Integer> a(new Integer{ 10 });
	shared_ptr<Integer> b(new Integer{ 20 });
	shared_ptr<Integer> c = a;
	shared_ptr<Integer> d(new Integer{ 30 });
	shared_ptr<Integer> e = b;
	a = d;
	b = shared_ptr<Integer>(new Integer(40));
	shared_ptr<Integer> f = c;
	b = f;
	
	printf("%d\n", a->get());
	printf("%d\n", b->get());
	printf("%d\n", c->get());
	printf("%d\n", d->get());
	printf("%d\n", e->get());
	printf("%d\n", f->get());
}

The output is similar to this one:


Deleting 40
30
10
10
30
20
10
Deleting 20
Deleting 10
Deleting 30

Result: No crashes and all the objects are released when they are not being used anymore.

In order to hide the operator new and to provide an optimization while allocating the object to be shared, the variadic template function make_shared was created. It is a template function that performs three tasks:

  1. Allocates contiguous memory for the object and for the reference counter. This makes the creation and destruction of objects faster because only one allocation and deallocation will be needed when creating the object to be shared and its reference counter.
  2. Invokes to the constructor of the class being instantiated forwarding the arguments used when this function was invoked.
  3. Returns a shared_ptr to the newly created object.

make_shared<T> is a variadic template function that receives as arguments, the arguments that the constructor of class T needs.

So, our main function will look like this:

int main()
{
	auto a = make_shared<Integer>(10);
	auto b = make_shared<Integer>(20);
	auto c = a;
	auto d = make_shared<Integer>(30);
	auto e = b;
	a = d;
	b = make_shared<Integer>(40);
	auto f = c;
	b = f;
	
	printf("%d\n", a->get());
	printf("%d\n", b->get());
	printf("%d\n", c->get());
	printf("%d\n", d->get());
	printf("%d\n", e->get());
	printf("%d\n", f->get());
}

What other uses can we think for shared_ptr?

Look at this C++03 code (I am reusing the Integer class):

#include <cstdio>
#include <cstring>

#include <vector>

using namespace std;

class Integer
{
	int n;
	public:
	  Integer(int n) : n(n) { }
	  ~Integer() { printf("Deleting %d\n", n); }
	  int get() const { return n; }
};

Integer* get_instance(int n)
{
	return new Integer(n);
}

int main()
{
	vector<Integer*> vec;
	
	for (int i = 0; i < 100; i++)
		vec.push_back(get_instance(i));
		
	int sum = 0;
	for (vector<Integer*>::const_iterator it = vec.begin(); it != vec.end(); ++it)
		sum += (*it)->get();
		
	//We do something with the elements of the vector	
	printf("Sum: %d\n", sum);
	
	//We need to release them manually
	for (vector<Integer*>::iterator it = vec.begin(); it != vec.end(); ++it)
		delete *it;
}

And compare it against this C++11 code:

#include <cstdio>
#include <cstring>

#include <vector>
#include <memory>

using namespace std;

class Integer
{
	int n;
	public:
	  Integer(int n) : n(n) { }
	  ~Integer() { printf("Deleting %d\n", n); }
	  int get() const { return n; }
};

shared_ptr<Integer> get_instance(int n)
{
	return make_shared<Integer>(n);
}

int main()
{
	vector<shared_ptr<Integer>> vec;
	
	for (int i = 0; i < 100; i++)
		vec.push_back(get_instance(i));
	
	//We do something with the elements of the vector		
	int sum = 0;
	for (auto& i : vec)
		sum += i->get();
		
	printf("Sum: %d\n", sum);
}

What do you see? These advantages of the C++11 version:

  • You can return shared_ptr instances from functions. This is far better than returning a naked pointer, because when returning a naked pointer, the programmer that invokes the function does not know a priori if he will need to use free, delete, a custom deallocator or nothing after using the object pointed by the returned pointer. When returning a shared_ptr instance, the programmer knows that the object will be released automatically when it will not be referred anymore.
  • You can store shared_ptr instances inside a STL container. This makes the memory management for your application easier, because you do not need to deallocate the objects manually anymore.
  • Because of the automatic memory deallocation, your program is exception safe. In the C++03 version, any problem occurring before the block in charge to release the pointers would leave a lot of objects leaking in the heap.
  • Though the C++11 is still using pointers, because of make_shared usage, the operator new is not used anymore.
  • Not related to shared_ptr; but auto and the range-based for loop make your program shorter and easier to write and understand :)

I based heavily my example in an example given by Herb Sutter in his talk: “(Not your father’s) C++”.

Shortcomings of shared_ptr? It does not work for circular references. If you want to implement something where circular references exist, you need to use std::weak_ptr. I will write about it in a new entry.

2 thoughts on “C++: Smart pointers, part 4: shared_ptr

  1. One not so well known use for shared_ptr is for RAII style execution of clean up code when the pointer goes out of scope.

    The function call to clean-up or do *whatever* code when the scope exists could look something like this.

    {
    std::shared_ptr RAII_cleaner(nullptr,
    [](void*) { ...... clean up code here ....}); } // RAII_cleaner or “whatever” executes at exit

    Unfortunately, this only works for shared_ptr, not unique_ptr

Leave a comment