tirsdag 26. august 2014

[ C++11 - Part 3 ] Smart pointers

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_ptrs can point to the object at a time.
    • Object is released when no share_ptrs 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 ifs 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_ptrs 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


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 versions VisualStudio 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
    • More on this below.

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 pFoo1
  • pFoo1 is unchanged
Foo* pFoo2 = nullptr;
pFoo2 = fooUPtr.release();
  • fooUPtr no longer manages the object pFoo1 points to
  • pFoo2 now points to the same object as pFoo1
But now 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 :

These work int the similar fashion
std::unique_ptr< Foo >( new Foo ) foo1;
foo1 = new Foo();
And
std::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_ptrs 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_ptrs.

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 ints, two std::strings or two objects of a class/struct or anything else you want.

The version used for unique_ptrs 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 manages foo2
  • 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 new Foo 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 foo1 is going to be managing foo2
This is the same as doing :
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 :
  • foo1 manages foo2 
  • foo2 manages foo1
In this case the result is.
  • foo1 manages foo2 
  • 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_ptrs 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_ptrs are managing this object, But for unique_ptrs 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_ptrs 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_ptrs are managing to the object
  • weak_ptr reference count
    • How many weak_ptrs are referring to this object
      • More on  weak_ptrs 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_ptrswill 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_ptrs, we can't do that before we make sure that no other shared_ptrs are managing it. So what we do is that we look on the control block and how many shared_ptrs 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_ptrs.

Changing the object being pointed to / reassignment


Similar to unique_ptrs 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's shared_ptr count is now 0, and the object it manages will be deleted
  • ptr1 and ptr2 will now both manage the same object as the original ptr2 with a shared_ptr count of 2

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
// Create shared_ptrs
std::shared_ptr< Foo > sharedPtr = std::make_shared< Foo >();
Foo* rawPtr = new Foo();

// Reassign
sharedPtr_ptr.reset( rawPtr );
Result
  • The shared_ptr reference count for sharedPtr is decremented as if we were calling the destructor.
    • The Foo object sharedPtr was originally manging may get deleted
  • sharedPtr now manages the object rawPtr 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_ptrs is the exact same as for swapping two unique_ptrs :
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_ptrs using the object is 0 regardless of how what the weak_ptrs 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_ptrs 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_ptrs because unique_ptrs 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_ptrs 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_ptrs 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 a shared_ptr that manages the same object as sharedPtr
  • 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 other shared_ptr should be destroyed in other threads in the meantime

Usage 2


Our shared_ptrs 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 the weak_ptra
  • 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_ptrs is the exact same as for swapping two shared_ptrs 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_ptrs. 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_ptrs 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, not shared_ptr
    • If weak_ptr count reaches 0 and the count for shared_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