unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers – Knowing your smart pointers (2/7)
This is episode 2 in y series Smart Developers Use Smart Pointers. The series contains:
- Smart pointer basics
- unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers: clearly stating your intentions by knowing your smart pointers
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to implement the pimpl idiom by using unique_ptr
- How to make a polymorphic clone in modern C++
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
Like we saw when discussing what smart pointers are about, some active decision has to be taken about how a smart pointer should be copied. Otherwise, a default copy constructor would likely lead to undefined behaviour.
It turns out that there are several valid ways to go about this, and this leads to a variety of smart pointers. And it is important to understand what these various smart pointers do because they are ways to express a design into your code, and therefore also to understand a design by reading code.
We see here the various types of pointers that exist out there, approximately sorted by decreasing order of usefulness (according to me):
- std::unique_ptr
- raw pointer
- std::shared_ptr
- std::weak_ptr
- boost::scoped_ptr
- std::auto_ptr
std::unique_ptr
As of this writing, this is the smart pointer to use by default. It came into the standard in C++11.
The semantics of std::unique_ptr
is that it is the sole owner of a memory resource. A std::unique_ptr
will hold a pointer and delete it in its destructor (unless you customize this, which is the topic of another post).
This allows you to express your intentions in an interface. Consider the following function:
std::unique_ptr<House> buildAHouse();
It tells you that it gives you a pointer to a house, of which you are the owner. No one else will delete this pointer except the unique_ptr
that is returned by the function. And since you get the ownership, this gives you confidence that you are free to modify the value of the pointed to object. Note that std::unique_ptr
is the preferred pointer to return from a factory function. Indeed, on the top of taking care of handling the memory, std::unique_ptr
wraps a normal pointer and is therefore compatible with polymorphism.
But this works the other way around too, by passing an std::unique_ptr
as a parameter:
class House { public: House(std::unique_ptr<PileOfWood> wood); ...
In this case, the house takes ownership of the PileOfWood
.
Note though that even when you receive a unique_ptr, you’re not guaranteed that no one else has access to this pointer. Indeed, if another context keeps a copy of the pointer inside your unique_ptr, then modifying the pointed to object through the unique_ptr object will of course impact this other context. But since you are the owner, you are allowed to safely modify the pointed to object, and the rest of the design should take this into account. If you don’t want this to happen, the way to express it is by using a unique_ptr to const:
std::unique_ptr<const House> buildAHouse(); // for some reason, I don't want you // to modify the house you're being passed
To ensure that there is only one unique_ptr that owns a memory resource, std::unique_ptr
cannot be copied. The ownership can however be transferred from one unique_ptr to another (which is how you can pass them or return them from a function) by moving a unique_ptr into another one.
A move can be achieved by returning an std::unique_ptr
by value from a function, or explicitly in code:
std::unique_ptr<int> p1 = std::make_unique(42); std::unique_ptr<int> p2 = move(p1); // now p2 hold the resource and p1 no longer hold anything
Raw pointers
“What?”, you may be thinking. “We’re talking about smart pointers, what are raw pointers doing here??”
Well, even if raw pointers are not smart pointers, they aren’t ‘dumb’ pointers either. In fact there are legitimate reasons to use them although these reasons don’t happen often. They share a lot with references, but the latter should be preferred except in some cases (but this is the topic of another post).
For now I only want to focus on what raw pointers and references express in code: raw pointers and references represent access to an object, but not ownership. In fact, this is the default way of passing objects to functions and methods:
void renderHouse(House const& house);
This is particularly relevant to note when you hold an object with a unique_ptr and want to pass it to an interface. You don’t pass the unique_ptr, nor a reference to it, but rather a reference to the pointed to object:
std::unique_ptr<House> house = buildAHouse(); renderHouse(*house);
std::shared_ptr
shared_ptr
entered the standard in C++11, but appeared in boost well before that.
A single memory resource can be held by several std::shared_ptr
s at the same time. The shared_ptrs internally maintain a count of how many of them there are holding the same resource, and when the last one is destroyed, it deletes the memory resource.
Therefore std::shared_ptr
allows copies, but with a reference-counting mechanism to make sure that every resource is deleted once and only once.
At first glance, std::shared_ptr
looks like the panacea for memory management, as it can be passed around and still maintain memory safety.
But std::shared_ptr
should not be used by default, for several reasons:
- Having several simultaneous holders of a resource makes for a more complex system than with one unique holder, like with
std::unique_ptr
. Even though anstd::unique_ptr
doesn’t prevent from accessing and modifying its resource, it sends a message that it is the priviledged owner of a resource. For this reason you’d expect it to centralize the control of the resource, at least to some degree. - Having several simultaneous holders of a resource makes thread-safety harder,
- It makes the code counter-intuitive when an object is not shared in terms of the domain and still appears as “shared” in the code for a technical reason,
- It can incur a performance cost, both in time and memory, because of the bookkeeping related to the reference-counting.
One good case for using std::shared_ptr
though is when objects are shared in the domain. Using shared pointers then reflects it in an expressive way. Typically, the nodes of a graphs are well represented as shared pointers, because several nodes can hold a reference to one other node.
std::weak_ptr
weak_ptr
entered the language in C++11 but appeared in boost well before that.
std::weak_ptr
s can hold a reference to a shared object along with other std::shared_ptr
s, but they don’t increment the reference count. This means that if no more std::shared_ptr
are holding an object, this object will be deleted even if some weak pointers still point to it.
For this reason, a weak pointer needs to check if the object it points to is still alive. To do this, it has to be copied into to a std::shared_ptr
:
void useMyWeakPointer(std::weak_ptr<int> wp) { if (std::shared_ptr<int> sp = wp.lock()) { // the resource is still here and can be used } else { // the resource is no longer here } }
A typical use case for this is about breaking shared_ptr circular references. Consider the following code:
struct House { std::shared_ptr<House> neighbour; }; std::shared_ptr<House> house1 = std::make_shared<House>(); std::shared_ptr<House> house2 = std::make_shared<House>();; house1->neighbour = house2; house2->neighbour = house1;
None of the houses ends up being destroyed at the end of this code, because the shared_ptrs points into one another. But if one is a weak_ptr instead, there is no longer a circular reference.
Another use case pointed out by this answer on Stack Overflow is that weak_ptr can be used to maintain a cache. The data may or may not have been cleared from the cache, and the weak_ptr references this data.
boost::scoped_ptr
scoped_ptr
is present in boost but was not included in the standard.
It simply disables the copy and even the move construction. So it is the sole owner of a resource, and its ownership cannot be transferred. Therefore, a scoped_ptr can only live inside… a scope. Or as a data member of an object. And of course, as a smart pointer, it keeps the advantage of deleting its underlying pointer in its destructor.
std::auto_ptr
auto_ptr
was present in C++98, has been deprecated in C++11 and removed from the language in C++17.
It aimed at filling the same need as unique_ptr
, but back when move semantics didn’t exist in C++. It essentially does in its copy constructor what unique_ptr does in its move constructor. But auto_ptr is inferior to unique_ptr and you shouldn’t use it if you have access to unique_ptr, because it can lead to erroneous code:
std::auto_ptr<int> p1(new int(42)); std::auto_ptr<int> p2 = p1; // it looks like p2 == p1, but no! p1 is now empty and p2 uses the resource
You know Andersen’s The Ugly Duckling, where a poor little ducking is rejected by its siblings because it’s not good-looking, and who turns out to grow into a beautiful swan? The story of std::auto_ptr is like this but going back in time: std::auto_ptr started by being the way to go to deal with ownership, and now it looks terrible in front of its siblings. It’s like The Ugly Benjamin Button Duckling, if you will.
🙂
Stay tuned as in the next episode of this series we will see how to simplify complex memory management by using the more advanced features of std::unique_ptr
.
Related articles:
- Smart pointer basics
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to implement the pimpl idiom by using unique_ptr
- How to make a polymorphic clone in modern C++
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
Share this post!