top of page
altaybrusan

smart pointers


The code we just described is fully functional, but, it can be strengthened, specifically with the function, AlbumDao::albums(). In this function, we iterate through the database rows and create a new Album to fill a list. We can zoom in on this specific code section:

QVector<Album*> list; while(query.next()) { Album* album = new Album(); album->setId(query.value("id").toInt()); album->setName(query.value("name").toString()); list.append(album); } return list;

Let's say that the name column has been renamed to title. If you forget to update query.value("name"), you might run into trouble. The Qt framework does not rely on exceptions, but this cannot be said for every API available in the wild. An exception here would cause a memory leak: the Album* album function has been allocated on the heap but not released. To handle this, you would have to surround the risky code with a

try-catch statement and deallocate the album parameter if an exception has been thrown. Maybe this error should bubble up; hence, your try-catch statement is only there to handle the potential memory leak. Can you picture the spaghetti code weaving in front of you?

The real issue with pointers is the uncertainty of their ownership. Once it has been allocated, who is the owner of a pointer? Who is responsible for deallocating the object? When you pass a pointer as a parameter, when does the caller retain the ownership or release it to the callee?

Since C++11, a major milestone has been reached in memory management: the smart pointer feature has been stabilized and can greatly improve the safety of your code. The goal is to explicitly indicate the ownership of a pointer through simple template semantics. There are three types of smart pointer:

  • The unique_ptr pointer indicates that the owner is the only owner of the pointer

  • The shared_ptr pointer indicates that the pointer's ownership is shared among several clients

  • The weak_ptr pointer indicates that the pointer does not belong to the client

For now, we will focus on the unique_ptr pointer to understand smart pointer mechanics. A unique_ptr pointer is simply a variable allocated on the stack that takes the ownership of the pointer you provide with it. Let's allocate an Album with this semantic:

#include <memory> void foo() { Album* albumPointer = new Album(); std::unique_ptr<Album> album(albumPointer); album->setName("Unique Album"); }

The whole smart pointer API is available in the memory header. When we declared album as a unique_ptr, we did two things:

  • We allocated on the stack a unique_ptr<Album>. The unique_ptr pointer relies on templates to check at compile time the validity of the pointer type.

  • We granted the ownership of albumPointer memory to album. From this point on, album is the owner of the pointer.

This simple line has important ramifications. First and foremost, you do not have to worry anymore about the pointer life cycle. Because a unique_ptr pointer is allocated on the stack, it will be destroyed as soon as it goes out of scope. In this example, when we exit foo(), album will be removed from the stack. The unique_ptr implementation will take care of calling the Album destructor and deallocating the memory. Secondly, you explicitly indicate the ownership of your pointer at compile time. Nobody can deallocate the albumPointer content if they do not voluntarily fiddle with your unique_ptr pointer. Your fellow developers will also know with a single glance who is the owner of your pointer. Note that, even though album is a type of unique_ptr<Album>, you can still call Album functions (for example, album->setName()) using the -> operator. This is possible thanks to the overload of this operator. The usage of the unique_ptr pointer becomes transparent.

Well, this use case is nice, but the purpose of a pointer is to be able to allocate a chunk of memory and share it. Let's say the foo() function allocates the album unique_ptr pointer and then transfers the ownership to bar(). This would look like this:

void foo() { std::unique_ptr<Album> album(new Album()); bar(std::move(album)); } void bar(std::unique_ptr<Album> barAlbum) { qDebug() << "Album name" << barAlbum->name(); }

Here, we introduce the std::move() function: its goal is to transfer the ownership of a unique_ptr function. Once bar(std::move(album)) has been called, album becomes invalid. You can test it with a simple

if statement:

if (album) { ... }.

From now on, the bar() function becomes the owner of the pointer (through barAlbum) by allocating a new unique_ptr on the stack and it will deallocate the pointer on its exit. You do not have to worry about the cost of a unique_ptr pointer, as these objects are very lightweight and it is unlikely that they will affect the performance of your application. Again, the signature of bar() tells the developer that this function expects to take the ownership of the passed Album. Trying to pass around unique_ptr without the move() function will lead to a compile error. Another thing to note is the different meanings of the . (dot) and the -> (arrow) when working with a

unique_ptr pointer:

  • The -> operator dereferences to the pointer members and lets your call function on your real object

  • The . operator gives you access to the unique_ptr object functions

The unique_ptr pointer provides various functions. Among the most important are:

  • The get() function returns the raw pointer. The album.get() returns an Album* value.

  • The release() function releases the ownership of the pointer and returns the raw pointer. The album.release() function returns an Album* value.

  • The reset(pointer p = pointer()) function destroys the currently managed pointer and takes ownership of the given parameter. An example would be the barAlbum.reset() function, which destroys the currently owned Album*. With a parameter, barAlbum.reset(new Album()) also destroys the owned object and takes the ownership of the provided parameter.

Finally, you can dereference the object with the

* operation, meaning *album will return an Album& value. This dereferencing is convenient, but you will see that the more a smart pointer is used, the less you will need it. Most of the time, you will replace a raw pointer with the following syntax:

void bar(std::unique_ptr<Album>& barAlbum);

Because we pass the unique_ptr by reference, bar() does not take ownership of the pointer and will not try do deallocate it upon its exit. With this, there is no need to use move(album) in foo(); the bar() function will just do operations on the album parameter but will not take its ownership.

Now, let's consider shared_ptr. A shared_ptr pointer keeps a reference counter on a pointer. Each time a shared_ptr pointer references the same object, the counter is incremented; when this shared_ptr pointer goes out of scope, the counter is decremented. When the counter reaches zero, the object is deallocated.Let's rewrite our foo() bar() example with a shared_ptr pointer:

#include <memory> void foo() { std::shared_ptr<Album> album(new Album()); // ref counter = 1 bar(album); // ref counter = 2 } // ref counter = 0 void bar(std::shared_ptr<Album> barAlbum) { qDebug() << "Album name" << barAlbum->name(); } // ref counter = 1

As you can see, the syntax is very similar to the unique_ptr pointer. The reference counter is incremented each time a new shared_ptr pointer is allocated and points to the same data, and is decremented on the function exit. You can check the current count by calling the album.use_count() function.

The last smart pointer we will cover is the weak_ptr pointer. As the name suggests, it does not take any ownership or increment the reference counter. When a function specifies a weak_ptr, it indicates to the callers that it is just a client and not an owner of the pointer. If we re implement bar() with a weak_ptr pointer, we get:

#include <memory> void foo() { std::shared_ptr<Album> album(new Album()); // ref counter = 1 bar(std::weak_ptr<Album>(album)); // ref counter = 1 } // ref counter = 0 void bar(std::weak_ptr<Album> barAlbum) { qDebug() << "Album name" << barAlbum->name(); } // ref counter = 1

If the story stopped here, there would not be any interest in using a weak_ptr versus a raw pointer. The weak_ptr has a major advantage for the dangling pointer issue. If you are building a cache, you typically do not want to keep strong references to your object. On the other hand, you want to know if the objects are still valid. By using weak_ptr, you know when an object has been deallocated. Now, consider the raw pointer approach: your pointer might be invalid but you do not know the state of the memory.

There is another semantic introduced in C++14 that we have to cover:

make_unique. This keyword aims to replace the new keyword and construct a unique_ptr object in an exception-safe manner. This is how it is used: unique_ptr<Album> album = make_unique<Album>();

The make_unique keyword wraps the

new keyword to make it exception-safe, specifically in this situation:

foo(new Album(), new Picture())

This code will be executed in the following order:

  1. Allocate and construct the Album function.

  2. Allocate and construct the Picture function.

  3. Execute the foo() function.

If new Picture() throws an exception, the memory allocated by new Album() will be leaked. This is fixed by using the make_unique keyword:

foo(make_unique<Album>(), make_unique<Picture>())

The make_unique keyword returns a unique_ptr pointer; the C++ standard committee also provided an equivalent for shared_ptr in the form of make_shared, which follows the same principle.

All these new C++ semantics try very hard to get rid of new and delete. Yet, it may be cumbersome to write all the unique_ptr and make_unique stuff. The auto keyword comes to the rescue in our album creation:

auto album = make_unique<Album>()

This is a radical departure from the common C++ syntax. The variable type is deduced, there is no explicit pointer, and the memory is automatically managed. After some time with smart pointers, you will see fewer and fewer raw pointers in your code (and even fewer delete, which is such a relief). The remaining raw pointers will simply indicate that a client is using the pointer but does not own it.

Overall, C++11 and C++14 smart pointers are a real step up in C++ code writing. Before them, the bigger the code base, the more insecure we felt about memory management. Our brain is just bad at properly grasping complexity at such a level. Smart pointers simply make you feel safe about what you write. On the other hand, you retain full control of the memory. For performance-critical code, you can always handle the memory yourself. For everything else, smart pointers are an elegant way of explicitly indicating your object's ownership and freeing your mind.

We are now equipped to rewrite the little insecure snippet in the AlbumDao::albums() function. Update AlbumDao::albums() like so:

// In AlbumDao.h std::unique_ptr<std::vector<std::unique_ptr<Album>>> albums() const; // In AlbumDao.cpp unique_ptr<vector<unique_ptr<Album>>> AlbumDao::albums() const { QSqlQuery query("SELECT * FROM albums", mDatabase); query.exec(); unique_ptr<vector<unique_ptr<Album>>> list(new vector<unique_ptr<Album>>()); while(query.next()) { unique_ptr<Album> album(new Album()); album->setId(query.value("id").toInt()); album->setName(query.value("name").toString()); list->push_back(move(album)); } return list; }

Wow! The signature of the album() function has turned into something very peculiar. Smart pointers are supposed to make your life easier, right? Let's break it down to understand a major point of smart pointers with Qt: container behavior.

The initial goal of the rewrite was to secure the creation of album. We want the list to be the explicit owner of the album. This would have changed our list type (that is albums() return type) to QVector<unique_ptr<Album>>. However, when the list type is returned, its elements will be copied (remember, we previously defined the return type to QVector<Album>). A natural way out of this would be to return a QVector<unique_ptr<Album>>* type to retain the uniqueness of our Album elements.

Behold, here lies a major pain: the QVector class overloads the copy operator. Hence, when the list type is returned, the uniqueness of our unique_ptr elements cannot be guaranteed by the compiler and it will throw a compile error. This is why we have to resort to a vector object coming from the standard library and write the long type:

unique_ptr<vector<unique_ptr<Album>>>.

Note

Take a look at the official response for support of the unique_ptr pointer in the Qt container. It is clear beyond any possible doubt:

http://lists.qt-project.org/pipermail/interest/2013-July/007776.html. The short answer is: no, it will never be done. Do not even mention it. Ever. If we translate this new albums() signature into plain English it will read: the album() function returns a vector of Album. This vector is the owner of the Album elements it contains and you will be the owner of the vector.

To finish covering this implementation of albums(), you may notice that we did not use the auto and make_unique keywords for the list declaration. Our library will be used on a mobile in Chapter 5, Dominating the Mobile UI, and C++14 is not yet supported on this platform. Therefore, we have to restrain our code to C++11. We also encounter the use of the move function in the instruction list->push_back(move(album)). Until that line, the album is "owned" by the while scope, the move gives the ownership to the list. At the last instruction, return list, we should have written move(list), but C++11 accepts the direct return and will automatically make the move() function for us.

What we covered in this section is that the AlbumDao class is completely matched in PictureDao. Please refer to the source code of the chapter to see the full PictureDao class implementation

Smart pointers are one way to address these issues. Smart pointers are wrappers

around raw pointers that act much like the raw pointers they wrap, but that avoid

many of their pitfalls. You should therefore prefer smart pointers to raw pointers.

Smart pointers can do virtually everything raw pointers can, but with far fewer

opportunities for error.

There are four smart pointers in C++11: std::auto_ptr, std::unique_ptr,

std::shared_ptr, and std::weak_ptr. All are designed to help manage the lifetimes

of dynamically allocated objects, i.e., to avoid resource leaks by ensuring that

such objects are destroyed in the appropriate manner at the appropriate time (including

in the event of exceptions).

std::auto_ptr is a deprecated leftover from C++98. It was an attempt to standardize

what later became C++11’s std::unique_ptr. Doing the job right required move

semantics, but C++98 didn’t have them. As a workaround, std::auto_ptr co-opted

its copy operations for moves. This led to surprising code (copying a std::auto_ptr

sets it to null!) and frustrating usage restrictions (e.g., it’s not possible to store

std::auto_ptrs in containers).

std::unique_ptr does everything std::auto_ptr does, plus more. It does it as efficiently,

and it does it without warping what it means to copy an object. It’s better

than std::auto_ptr in every way. The only legitimate use case for std::auto_ptr

is a need to compile code with C++98 compilers. Unless you have that constraint,

you should replace std::auto_ptr with std::unique_ptr and never look back.

The smart pointer APIs are remarkably varied. About the only functionality common

to all is default construction. Because comprehensive references for these APIs are

widely available, I’ll focus my discussions on information that’s often missing from

API overviews, e.g., noteworthy use cases, runtime cost analyses, etc. Mastering such

information can be the difference between merely using these smart pointers and

using them effectively.

std::unique_ptr embodies exclusive ownership semantics. A non-null std::

unique_ptr always owns what it points to. Moving a std::unique_ptr transfers

ownership from the source pointer to the destination pointer. (The source pointer is

set to null.) Copying a std::unique_ptr isn’t allowed, because if you could copy a

std::unique_ptr, you’d end up with two std::unique_ptrs to the same resource,

each thinking it owned (and should therefore destroy) that resource.

std::unique_ptr is thus a move-only type. Upon destruction, a non-null

std::unique_ptr destroys its resource.

Attempting to assign a raw pointer (e.g., from new) to a std::unique_ptr won’t

compile, because it would constitute an implicit conversion from a raw to a

smart pointer. Such implicit conversions can be problematic, so C++11’s smart

pointers prohibit them. That’s why reset is used to have pInv assume ownership

of the object created via new.

auto fun(int i)->decltype(i)

{

return i*5;

}

The use of auto before the function name has nothing to do with type deduction.

Rather, it indicates that C++11’s trailing return type syntax is being used, i.e., that the

function’s return type will be declared following the parameter list (after the “->”). A

trailing return type has the advantage that the function’s parameters can be used in

the specification of the return type.

std::unique_ptr comes in two forms, one for individual objects (std::

unique_ptr<T>) and one for arrays (std::unique_ptr<T[]>). As a result, there’s

never any ambiguity about what kind of entity a std::unique_ptr points to. The

std::unique_ptr API is designed to match the form you’re using. For example,

there’s no indexing operator (operator[]) for the single-object form, while the array

form lacks dereferencing operators (operator* and operator->).

The existence of std::unique_ptr for arrays should be of only intellectual interest

to you, because std::array, std::vector, and std::string are virtually always

better data structure choices than raw arrays. About the only situation I can conceive

of when a std::unique_ptr<T[]> would make sense would be when you’re using a

C-like API that returns a raw pointer to a heap array that you assume ownership of.

std::unique_ptr is a small, fast, move-only smart pointer for managing

resources with exclusive-ownership semantics.

• By default, resource destruction takes place via delete, but custom deleters

can be specified. Stateful deleters and function pointers as deleters increase the

size of std::unique_ptr objects.

• Converting a std::unique_ptr to a std::shared_ptr is easy.

int i[]={4,5};

unique_ptr<int> _uni1(i);

unique_ptr<int> _uni2;

unique_ptr<int>& _uni3 = _uni1;

qDebug()<<"Unique pointer 1 is valid before move: "<< (_uni1?"true":"false");

qDebug()<<"Unique pointer 2 is valid before move: "<< (_uni2?"true":"false");

qDebug()<<"Unique pointer 3 is valid before move: "<< (_uni3?"true":"false");

_uni2 = move(_uni1);

qDebug()<<"Unique pointer 1 is valid after move: "<< (_uni1?"true":"false");

qDebug()<<"Unique pointer 2 is valid after move: "<< (_uni2?"true":"false");

qDebug()<<"Unique pointer 3 is valid after move: "<< (_uni3?"true":"false");


Screen clipping taken: 11/20/2018 11:12 AM

Use std::shared_ptr for shared-ownership

std::shared_ptr is the C++11 way of binding these worlds together. An object

accessed via std::shared_ptrs has its lifetime managed by those pointers through

shared ownership. No specific std::shared_ptr owns the object. Instead, all

std::shared_ptrs pointing to it collaborate to ensure its destruction at the point

where it’s no longer needed. When the last std::shared_ptr pointing to an object

stops pointing there (e.g., because the std::shared_ptr is destroyed or made to

point to a different object), that std::shared_ptr destroys the object it points to

Increments and decrements of the reference count must be atomic

Atomic operations

are typically slower than non-atomic operations, so even though reference

counts are usually only a word in size, you should assume that reading and writing

them is comparatively costly.

Move-constructing a std::shared_ptr from

another std::shared_ptr sets the source std::shared_ptr to null, and that means

that the old std::shared_ptr stops pointing to the resource at the moment the new

std::shared_ptr starts. As a result, no reference count manipulation is required.

Moving std::shared_ptrs is therefore faster than copying them: copying requires

incrementing the reference count, but moving doesn’t. This is as true for assignment

as for construction, so move construction is faster than copy construction, and move

assignment is faster than copy assignment.

Like std::unique_ptr (see Item 18), std::


Screen clipping taken: 11/21/2018 10:51 AM

In general,

it’s impossible for a function creating a std::shared_ptr to an object to know

whether some other std::shared_ptr already points to that object, so the following

rules for control block creation are used:

• std::make_shared (see Item 21) always creates a control block. It manufactures

a new object to point to, so there is certainly no control block for that

object at the time std::make_shared is called.

• A control block is created when a std::shared_ptr is constructed from a

unique-ownership pointer (i.e., a std::unique_ptr or std::auto_ptr).

Unique-ownership pointers don’t use control blocks, so there should be no control

block for the pointed-to object. (As part of its construction, the

std::shared_ptr assumes ownership of the pointed-to object, so the uniqueownership

pointer is set to null.)

A consequence of these rules is that constructing more than one std::shared_ptr

from a single raw pointer gives you a complimentary ride on the particle accelerator

of undefined behavior, because the pointed-to object will have multiple control

blocks. Multiple control blocks means multiple reference counts, and multiple reference

counts means the object will be destroyed multiple times (once for each reference

count).




Screen clipping taken: 11/21/2018 11:18 AM


Screen clipping taken: 11/21/2018 11:17 AM


Screen clipping taken: 11/21/2018 11:56 AM



The creation of the raw pointer pw to a dynamically allocated object is bad, because it

runs contrary to the advice behind this entire chapter: to prefer smart pointers to raw

pointers.

The second destruction is responsible for the undefined behavior.

There are at least two lessons regarding std::shared_ptr use here. First, try to

avoid passing raw pointers to a std::shared_ptr constructor. The usual alternative

is to use std::make_shared

pass a raw pointer to a std::shared_ptr constructor, pass the result of new directly

instead of going through a raw pointer variable.

Something else std::shared_ptrs can’t do is work with arrays. In yet another difference

from std::unique_ptr, std::shared_ptr has an API that’s designed only

for pointers to single objects. There’s no std::shared_ptr<T[]>. From time to

time, “clever” programmers stumble on the idea of using a std::shared_ptr<T> to

point to an array, specifying a custom deleter to perform an array delete (i.e., delete

[]). This can be made to compile, but it’s a horrible idea. For one thing,

std::shared_ptr offers no operator[], so indexing into the array requires awkward

expressions based on pointer arithmetic. For another, std::shared_ptr supports

derived-to-base pointer conversions that make sense for single objects, but that

open holes in the type system when applied to arrays. (For this reason, the

std::unique_ptr<T[]> API prohibits such conversions.) Most importantly, given

the variety of C++11 alternatives to built-in arrays (e.g., std::array, std::vector,

std::string), declaring a smart pointer to a dumb array is almost always a sign of

bad design.

If exclusive ownership will do

or even may do, std::unique_ptr is a better choice. Its performance profile is close

to that for raw pointers, and “upgrading” from std::unique_ptr to std::

shared_ptr is easy, because a std::shared_ptr can be created from a std::

unique_ptr.

The reverse is not true. Once you’ve turned lifetime management of a resource over

to a std::shared_ptr, there’s no changing your mind. Even if the reference count is

one, you can’t reclaim ownership of the resource in order to, say, have a

std::unique_ptr manage it. The ownership contract between a resource and the

std::shared_ptrs that point to it is of the ’til-death-do-us-part variety. No divorce,

no annulment, no dispensations.

1 view0 comments

Recent Posts

See All

Comments


bottom of page