This is the fourth post of several posts I wrote related to smart pointers:
- Smart pointers
- unique_ptr
- More on unique_ptr
- shared_ptr
- 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:
- 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.
- Invokes to the constructor of the class being instantiated forwarding the arguments used when this function was invoked.
- 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 ashared_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, theoperator new
is not used anymore. - Not related to
shared_ptr
; butauto
and the range-basedfor
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.
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
That is a very interesting usage actually! Thanks!