Memory in C++
One of the major parts of C++11 is the new smart pointers. But why do we need them, and why should you steer away from the old style pointers?
This post is intended both as an introduction, and a reference guide to the smart pointers found in
C++11
. It will cover what they are, how to use them and how they work.Memory allocation in C++
C++ supports
dynamic allocation
( new
/ delete
). It's a way of creating a object whenever you need it. The object will live until you call delete
on it. Otherwise it won't be deleted, not even when it goes out of scope. And if you don't delete it, none will, which in turns causes memory leaks. It might also cause undefined behavior ( which means anything can happen ) if, for instance, you delete something and try to use it afterwards.Smart pointer will help you with these issues and make your code cleaner and better.
Smart pointers
Smart pointers are like regular pointers, but they provide extra functionality such as automatic checks or other safety mechanisms. C++11 introduces three smart pointer types :
- unique_ptr
- Only one
unique_ptr
can point to the object at the time - shared_ptr
- Several
share_ptr
s can point to the object at a time. - Object is released when no
share_ptr
s point to the object - weak_ptr
- Non-owning, needs to be converted into an
shared_ptr
to use the object it points to
Using smart pointers
Once you have created the smart pointer, you can use it as a regular pointer. So operations like
!=
, !
, !
, ->
, *
etc. This is not true for weak_ptr
, but we'll get to that later. You can also use them in
if
s like this : if ( ptr )
this will return true as long as the smart pointer is initialized and is managing a valid object ( meaning you can use the pointer. )We'll now take an in-depth look at the smart pointers, one at the time.
unique_ptr
An
unique_ptr
is the sole owner of an object that it points to. Two unique_ptr
s can also swap objects, but they can never manage the same object. And as with all smart pointers, it takes care of deleting the object. Initialization and allocation
There are two ways of doing this, regular constructor or
make_shared
constructor
The constructor is pretty straight forwards. You just set the template arguments ( in this case
Foo
) and pass an allocated ( new
'ed ) object :std::unique_ptr< Foo> p1(new Foo);
This is pretty straight forwards ; template parameter is the type and you supply an allocated (
new
'ed ) object. make_unique
make_unique
The second alternative is using a function named
make_unique
, this is in most cases better, because it constructs the object and it helps prevent errors. It also follows the naming convention ofshared_ptr
which can be constructed in the same way with the make_shared
method.This is how to use
make_unique
: >std::unique_ptr< Foo > p2 = std::make_unique< Foo > ();
What
make_unique
does is that it creates a new unique_ptr
that manages a new object of the template type ( Foo
in our case. ) It takes the arguments of the make_unique
function itself and passes it to the constructor of the new Foo
. Then the function returns the unique_ptr
it created and the =operator
assigns it to the unique_ptr
we declared ( p2
)This function requires
C++14
, it should have been included in C++11
, a simple oversight lead to it not being included in the standard. But don't worry, it's easy to get this to compile:- On
Linux
/Mac
, you can do this by changing-std=c++11
to-std=c++1y
in your compile command - On
Windows
, newer versionsVisualStudio
should support this without you having to do anything.
Destruction and deallocation
Since the
unique_ptr
is a smart pointer, it handles deallocation automatically. In general, this means that the unique_ptr
calls delete
on the object it holds. This happens when :- The
unique_ptr
is being destroyed. - Happens when the
unique_ptr
goes out of scope. - You set the
unique_ptr
to point to something else
Releasing the object
Releasing the object means that the
unique_ptr
stops managing the object. This means you can let a regular pointer "take over" the object the unique_ptr
was managing. So when the unique_ptr
goes out of scope or is destroyed in any way, the pointer object it was managing wont be affected. Here's an example
Foo* pFoo1 = new Foo();
std::unique_ptr< Foo > fooUPtr( pFoo1);
fooUPtr
now manages pFoo1pFoo1
is unchanged
Foo* pFoo2 = nullptr;
pFoo2 = fooUPtr.release();
But now
fooUPtr
no longer manages the objectpFoo1
points topFoo2
now points to the same object aspFoo1
pFoo1
/pFoo2
needs to be deleted. Since they both point to the same object, we just need to delete one of them : delete pFoo1;
Changing the object being pointed to / reassignment
Changing what the
unique_ptr
points to is quite easy. It can be done in two ways :- Using the = operator
- Using the reset() function
These work int the similar fashion
std::unique_ptr< Foo >( new Foo ) foo1;
foo1 = new Foo();
Andstd::unique_ptr< Foo >( new Foo ) foo2;
foo2.reset( new Foo() );
Both of these properly deallocates the original object managed by
foo1
, calling its destructor in the process. Then the new object returned by new
is assigned to foo1
and foo1
now manages it just like it did with the previous object. Transferring ownership
You can't set two
unique_ptr
s to manage the same object, but you can transfer ownership from one unique_ptr
to another. And you can swap the managed object of two unique_ptr
s. Swapping owenership
Swapping ownership can be done in two ways ; a member function, or the
std::swap()
function.Member function
The member function is very straight forwards :
std::unique_ptr< Foo >( new Foo() ) foo1;
std::unique_ptr< Foo >( new Foo() ) foo2;
foo1.swap( foo2 );
std::swap
std::swap()
is a very simple, but also very useful algorithm defined in the algorithm
header ( until C++11
and utility
since C++11
). It basically swaps two objects of whatever type you pass as arguments. It can be two int
s, two std::string
s or two objects of a class
/struct
or anything else you want. The version used for
unique_ptr
s is just an overload of this that uses the above swap( std::unique_ptr )
member function function internally. Here's how to use it : std::unique_ptr< Foo >( new Foo() ) ;
std::unique_ptr< Foo >( new Foo() ) ;
std::swap(foo1, foo2 );
So in reality, the member function and
swap()
are identical. The result of both of these are as you would expect :foo1
now managesfoo2
foo2
now managesfoo1
Reseting
You can use the
reset()
to reset the unique_ptr
This means that we change what pointer the unique_ptr
manages, deleting the old one. This method can only be used with raw pointers, not smart pointers. Example :
std::unique_ptr< Foo> ptr( new Foo );
foo.reset( new Foo );
What this does
- Crates a new
uniqe_ptr
that manages a newFoo
object - Deletes the old managed
Foo
object and replaces it with a new one
Transfering owenership
You can use the
= operator
to transfer ownership :std::unique_ptr< Foo >(new Foo() ) foo1;
std::unique_ptr< Foo >(new Foo() ) foo2;
foo1 = foo2;
This takes the pointer from foo2
and gives it to foo1
. This means that : foo2
no longer handles any object ( like calling )- This is because only one
unique_ptr
can manage an object at a time - The object that
foo1
was holding is deleted - Because
is going to be managingfoo1
foo2
std::unique_ptr< Foo >(new Foo() ) foo1;
std::unique_ptr< Foo >(new Foo() ) foo2;
foo1.reset( foo2.release() );
Note that this is not the same as swapping two unique_ptr
If we swap, the result would be :
In this case the result is.
foo1
managesfoo2
foo2
managesfoo1
foo1
managesfoo2
foo2
doesn't manage any object
Destroying the object
When you want to destroy the object the
unique_ptr
manages, there are two ways :- Destructor
- This simply means letting the
unique_ptr
go out of scope - The
reset()
method - You can use this with either a
NULL
pointer or another pointer, the managed object will get deleted in both cases
The bool operator
The
bool operator
dictates what happens when used in a boolean
expression like if ()
. It will return true
if the unique_ptr
manages an object. If not, it'll return false
. In other words ; if, and only if, you can use it, it'll return true
, so this is exactly the same as you do when you're checking if a pointer is NULL
or nullptr
The other operators
The other operators works just like regular pointers, so I won't discuss them here.
shared_ptr
shared_ptr
is, as the name implies, a smart pointer that allows sharing. This means you can have several shared_ptr
s that point to the same object. This is not permitted by unique_ptr
as we saw in the previous section.Implementation
The
shared_ptr
is a bit more complex than unique_ptr
. This is because a shared_ptr
needs to keep track of how many other shared_ptr
s are managing this object
, But for unique_ptr
s there will always be only one pointer managing the same resource, so there is no need to keep track of the number of other unique_ptr
s that are managing this object ( because it's always just one! )In order to keep track of this information,
shared_ptr
keeps a pointer to a structure called a control block
. This structure has three member variables :-
shared_ptr
reference count - How many
shared_ptr
s are managing to the object weak_ptr
reference count- How many
weak
_ptr
s are referring to this object - More on
weak
_ptr
s later - A pointer to the object the
shared_ptr
manages
The
shared_ptr
also keeps a pointer to the object it manages. And this is were it gets a little complicated, because this pointer is closely related to the one in the control block. But why? The reason for this is a bit complicated and it has to do with the two ways of creating a shared_ptr
, make_shared
and constructor. I will discuss this in the next section :Initialization
When it comes to
unique_ptr
, the difference between using make_unique
and the regular constructor is slight ( that doesn't mean you shouldn't use make_unique
as often as you can! ) But in a shared_ptr
things are different. As stated above, a
shared_ptr
has two elements : a control block and a pointer to the object it manages. Both of these needs to be allocated, but before we get into that, let's look at how we can create a shared_ptr
: std::shared_ptr p1(new Foo);
std::shared_ptr< Foo > p2 = std::make_shared< Foo > ();
In the first example, we first allocate Foo
using new
before pass it to the shared_ptr
constructor. This means that the share_ptr
has no control over the allocation of Foo
so all it can do is to create the control block and have it point to Foo
. The figure below will show the procedure for when you create a shared_ptr
using constructor :Foo
and ControlBlock allocated in two steps
As you can see, the
control block
and the Foo
needs to be allocated in two steps. First the object, then the control bloc. But if we let
make_shared
handle the allocation of Foo
, it can allocate both the control block
and Foo
in one go. It'll look something like this :Foo
is now part of the FooControlBlock
So
make_shared
creates the object and the control block together in one operation. This makes the operation faster than creating them in two step, but it requires them to be one object, so here Foo
is part of the control block itself. make_shared
is available in C++11
so you can use it without enabling C++14
When not to use make_shared
?
There are two cases when you can't use
make_shared
:- If you are using a pointer that's already created somewhere else
- Using
make_shared
means the object would be re-allocated - Pass the pointer in the constructor instead ( where we passed
new Foo()
in the example above ) - If you don't want to use the default
delete
- You can't specify a custom deleter using
make_shared
- This is a bit complicated, so I won't go into details
Destruction and deallocation
The destructor for
shared_ptr
is also a bit different from unique_ptr
because an unique_ptr
will always be the sole manager of an object ( not other unique_ptr
or shared_ptr
swill be managing it. ) This means it's always safe to delete
, so that's what the unique_ptr
will do.But when it comes to
shared_ptr
s, we can't do that before we make sure that no other shared_ptr
s are managing it. So what we do is that we look on the control block and how many shared_ptr
s are managing it. If this is 0
, we are the last owner and we can safetely delete it. The
weak_ptr
reference count it not checked at this point. I'll get into why in the next section that discusses weak_ptr
and how the relate to shared_ptr
s.Changing the object being pointed to / reassignment
Similar to
unique_ptr
s but here we need to do some extra work. There's two different cases for this ; setting the shared_ptr
to be the same as another shared_ptr
and setting the shared_ptr
to manage a new pointer. In both of these cases, it will decrement the
shared_ptr
reference count in the control block. And if this count reaches 0
it will delete the object being pointed to ( but not necessarily the control block, more on this later. ) Assigning to a different shared_ptr
Assigning a
shared_ptr
to a different shared_ptr
is done using the =operator
.Here's a simple example
// Create shared_ptrs
std::shared_ptr< Foo > ptr1 = std::make_shared< Foo >();
std::shared_ptr< Foo > ptr2 = std::make_shared< Foo >();
// Reassign
ptr2 = ptr1;
Result - The original
ptr1
'sshared_ptr
count is now0
, and the object it manages will be deleted ptr1
andptr2
will now both manage the same object as the originalptr2
with ashared_ptr
count of2
Assigning shared_ptr
to a new object
Assigning a
shared_ptr
to a new object/pointer is done using the reset()
function :Here's a simple example
Result// Create shared_ptrs
std::shared_ptr< Foo > sharedPtr = std::make_shared< Foo >();
Foo* rawPtr = new Foo();
// Reassign
sharedPtr
_ptr.reset( rawPtr );
- The
shared_ptr
reference count forsharedPtr
is decremented as if we were calling the destructor. - The
Foo
objectsharedPtr
was originally manging may get deleted sharedPtr
now manages the objectrawPtr
points to.
As you can see from the examples, you use
opeator=
for reassigning to another shared_ptr
but reset()
for reassigning to a different raw pointer. You can't use them the other way around. This can help prevent bugs by giving an error if the programmer uses the wrong versions. There is a way you can use
operator=
to assign to a new pointer; using make_shared< Foo >
to create a new object : std::shared_ptr< Foo >( new Foo ) foo2;
foo2 = make_shared< Foo >();
This works because make_shared
creates and returns a fully constructed shared_ptr
( just like make_unique
described above ) and the =operator
assigns it just like in the example above.Swapping
The syntax for swapping
shared_ptr
s is the exact same as for swapping two unique_ptr
s : std::shared_ptr< Foo >( new Foo ) foo2;
std::shared_ptr< Foo >( new Foo ) foo2;
Member function :foo1.swap( foo2 );
std::swap :std::swap( foo1, foo2 );
This will, as you would expect, swap both the control block and pointer for both the shared_ptr
. It needs to swap the control block since this is what keeps tracks of the number of references to the pointer so these needs to be consistent. The bool operator
The
bool operator
dictates what happens when used in a boolean
expression like if ()
. It will return true
if the shared_ptr
manages an object. If not, it'll return false
. In other words ; if, and only if, you can use it, it'll return true
, so this is exactly the same as you do when you're checking if a pointer is NULL
or nullptr
This is exactly the same as for
unique_ptr
.The other operators
The other operators, just like with
unique_ptr
, works just like regular pointers, so I won't discuss them here.weak_ptr
As mentioned in the introduction,
weak_ptr
doesn't actually manage a pointer. It just holds a non-owning ( "weak" ) reference
to an object that is managed by a shared_ptr
. It also keeps a pointer to the control block ( the exact same as the one in the shared_ptr
who manages the object. This means it has to be created from a shared_ptr
so that it can get a pointer to the control block. wrak_ptr
and the control block
The control block, as we saw in the previous section, keeps a count of both
shared_ptr
and weak_ptr
who's using the object. We also saw that the object will get deleted if the count of shared_ptr
s using the object is 0
regardless of how what the weak_ptr
s count is. This is part of the point of weak_ptr
; it is not supposed to keep objects alive except for in situations we explicitly tell it to. But even though the managed object will get deleted if the count of
shared_ptr
s is 0, the control block will remain intact. The control block will only be deleted if both the conut of shared_ptr
and weak_ptr
uses. This is because the weak_ptr
uses the control block to check if the object is alive. Creating a weak_ptr
There are two ways of creating a
weak_ptr
from a shared_ptr
: constructor and =operator
. This is very straight forwards : std::shared_ptr< int > sharedPtr = std::make_shared( 42 );
// 1. Constructor
std::weak_ptr< int > weakPtr1( sharedPtr );
// 2. = operator
std::weak_ptr< int > weakPtr2 = sharedPtr;
You can also create one from another weak_ptr
:// 3. Constuctor - weak_ptr
std::weak_ptr< Foo > weakPtr3( weakPtr1 );
// 4. = operator - weak_prt
std::weak_ptr< Foo > weakPtr4 = weakPtr2;
/code>
All of these will set up the pointer and the control block and increment weak count by 1. Creating a
weak_ptr
from a raw pointer won't work simply because it isn't designed to be managing a pointer by itself. And you can't use them with unique_ptr
s because unique_ptr
s are supposed to be the sole owner. Reseting a weak_ptr
The object a
weak_ptr
can be reset ( so that it no longer references any object ) using the destructor or the reset()
function.
// Set up a weak_ptr
std::shared_ptr< Foo > sharedPtr = std::make_shared< Foo >();
std::weak_ptr< Foo > weakPtr = sharedPtr;
// Reset it
weakPtr.reset()
Using a weak_ptr
weak_ptr
has the function lock()
. What this function does is that it makes a shared_ptr
of itself. This shared_ptr
will work exactly as any other shared_ptr
s maintaining the object. It will increase the shared_ptr
count, so this shared_ptr
will keep the object alive. If the object has been deleted, it will still return a
shared_ptr
. But this shared_ptr
doesn't point to anything, it's essentially a nullptr
. It will return false
if you use it in a if
or loop condition, just like a regular shared_ptr
or unique_ptr
that doesn't manage an object.This function is the crucial part of the
weak_ptr
. It enables us to keep weak_ptr
s that refers to object maintained by a shared_ptr
without preventing it from being deleted and we can still use it safely when we want. So let's look at an example :
Usage 1
Here everything works as intended. The
weak_ptr
refers to sharedPtr
which has a valid object inside this scope. lock()
returns ashared_ptr
that manages the same object assharedPtr
- The returned
shared_ptr
is valid so it enters the if - While in this if, the
shared_ptr
,fromWeakPtr1
keeps the object alive, even if all othershared_ptr
should be destroyed in other threads in the meantime
Usage 2
Our
shared_ptr
s has gone out of scope ( both the original and the one we created from the weak_ptr
and the object has been deleted. But the weak_ptr
, weakPtr
still exists as it was declared out of the scope. So we try to use it again :- We create our
shared_ptr
from theweak_ptr
a - The
shared
is created, but since the original object was deleted, this is essentially a nullptr - The
shared_ptr
returns false and it doesn't enter the if.
Leaving the scope
Finally we leave the scope and we delete
weakPtr
. Now the weak_ptr
count will be 0
and the control block will be deleted ( shared_ptr
count was already 0
)Releasing the object
A
weak_ptr
can't release the object because it doesn't have control over it, and the object might be NULL
. For this reason, weak_ptr
does not have a release()
function. The closest thing is the reset()
function described above.Swapping
The syntax for swapping
weak_ptr
s is the exact same as for swapping two shared_ptr
s and code>unique_ptrs: std::weak_ptr< Foo >( new Foo ) foo2;
std::weak_ptr< Foo >( new Foo ) foo2;
Member function :foo1.swap( foo2 );
std::swap :std::swap( foo1, foo2 );
This will, as you would expect, swap both the control block and pointer for both the
weak_ptr
s. It needs to swap the control block since this is what keeps tracks of the number of references to the pointer so these needs to be consistent.Changing the object being pointed to / reassignment
Similar to
shared_ptr
s but with a few differences :- Since we are not managing the object, we don't need to worry about deleting it.
- It might already have been deleted at this point, but this will not cause the opeartion to fail
- Decrement count for
weak_ptr
, notshared_ptr
- If
weak_ptr
count reaches0
and the count forshared_ptr
is also 0, we delete the control block - Now we can set the pointer to the object and control block
- Both of these will already have been created by a
shared_ptr
You can create reassign a
weak_ptr
to both a shared_ptr
and another weak_ptr
.For a full list of my tutorials / posts, click here. Feel free to comment if you have anything to say, spot any errors or have any suggestion or questions. I always appreciate getting comments.
Ingen kommentarer:
Legg inn en kommentar