The Pitfalls of Aliasing Pointers in Modern C++
This is guest post written by a guest author Benjamin Bourdin. If you’re also interested to share your ideas on Fluent C++, check out our guest posting area.
With the advent of smart pointers in Modern C++, we see less and less of the low-level concerns of memory management in our business code. And for the better.
To go further in this direction, we could be tempted to make the names of smart pointers themselves disappear: unique_ptr
, shared_ptr
… Maybe you don’t want to know those details, and only care about that an object is a “pointer that deals with memory management”, rather that the exact type of pointer it is:
using MyClassPtr = std::unique_ptr<MyClass>;
I’ve seen that sort of code at multiple occasions, and maybe you have this in your codebase too. But there are several issues with this practice, that make it not such a good idea. The following presents the argument against aliasing pointer types, and if you have an opinion we’d be glad to hear it in the comments section!
Smart pointers
Let’s make a quick recap on smart pointers. The point here is not to enumerate all the types of smart pointers C++ has, but rather to refresh to your memory on the basic usages of smart pointers that will have issues when using an alias. If youre memory is already fresh on smart pointers, you can safely skip to the next section.
std::unique_ptr
std::unique_ptr
is probably the most commonly used smart pointer. It represents the unique owner of a memory resource. The (C++14) standard way to create a std::unique_ptr
is to use std::make_unique
:
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(0, "hi");
std::make_unique
performs a perfect forwarding of its parameters to the constructor of MyClass
. std::unique_ptr
also accepts raw pointers, but that’s not the recommended practice:
std::unique_ptr<MyClass> ptr(new MyClass(0, "hi"));
Indeed, in certain cases it can lead to memory leaks, and one of the goals of smart pointers is to get rid of new
and delete
in business code.
Functions (or, more frequently, class methods) can acquire the ownership of the memory resource of a std::unique_ptr
. To do this, they take a std::unique_ptr
by value:
void fct_unique_ptr(std::unique_ptr<MyClass> ptr);
To pass arguments to this function, we have to invoke the move constructor of std::unique_ptr
and therefore pass it an rvalue, because std::unique_ptr
doesn’t have a copy constructor. The idea is that the move constructor transfers the ownership from the object moved-from to the object moved-to.
We can invoke it this way:
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(0, "hi"); fct_unique_ptr(std::move(ptr)); // 1st way fct_unique_ptr(std::make_unique<MyClass>(0, "hi")); // 2nd way fct_unique_ptr(std::unique_ptr<MyClass>(new MyClass(0, "hi"))); // 3rd way (compiles, but not recommended to use new)
std::shared_ptr
A std::shared_ptr
is a pointer that can share the ownership of a memory resource with other std::shared_ptr
s.
The (C++11) standard way to create std::shared_ptr
s is by using std::make_shared
:
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(0, "hi");
Like std::make_unique
, std::make_shared
perfect forwards its arguments to the constructor of MyClass
. And like std::unique_ptr
, std::shared_ptr
can be built from a raw pointer, and that is not recommended either.
Another reason to use std::make_shared
is that it can be more efficient than building a std::shared_ptr
from a raw pointer. Indeed, a shared pointer has a reference counter, and with std::make_shared
it can be constructed with the MyClass
object all in one heap allocation, whereas creating the raw pointer and then the std::shared_ptr
requires two heap allocations.
To share the ownership of a resource with a function (or, more likely, a class method), we pass a std::shared_ptr
by value:
void fct_shared_ptr(std::shared_ptr<MyClass> ptr);
But contrary to std::unique_ptr
, std::shared_ptr
accepts lvalues, and the copy constructor then creates a additional std::shared_ptr
that refers to the memory resource:
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(0, "hi"); fct_shared_ptr(ptr);
Passing rvalues would not make sense in this case.
Alias to pointer: danger!
Back to the question of aliasing pointer types, are the following aliases good practice?
using MyClassPtr = std::unique_ptr<MyClass>;
or
using MyClassPtr = std::shared_ptr<MyClass>;
Throughout the above examples, we’ve seen different semantics and usages for the various smart pointers. As a result, hiding the type of the smart pointers behind an alias leads to issues.
What sort of issues? The first one is that we lose the information about ownership. To illustrate, consider the following function:
void do_something(MyClassPtr handler);
As a reader of the function, I don’t know what this call means: is it a transfer of ownerhip? Is it a sharing of ownership? Is it simply passing an pointer to access its underlying resource?
As the maintainer of the function, I don’t know what exactly I’m allowed to do with that pointer: can I safely store the pointer in a object? As its name suggests, is MyClassPtr
a simple raw pointer, or is it a smart pointer? I have to go look at what is behind the alias, which reduces the interest of having an alias.
And as a user of the function, I don’t know what to pass to it. If I have a std::unique_ptr<MyClass>
, can I pass it to the function? And what if I have a std::shared_ptr<MyClass>
? And even if I have a MyClassPtr
, of the same type of the parameter of do_something
, should I copy it or move it when passing it to do_something
? And to instantiate a MyClassPtr
, should we use std::make_unique
? std::make_shared
? new
?
A too high level of abstraction
In all the above situations (maintenance, function calls, instantiations), using an alias can force us to go look what it refers to, making the alias a problem rather than a help. It’s a bit like a function whose name would not be enough to understand it, and that would require you to go look at its implementation to understand what it does.
The intention behind aliasing a smart pointer is a noble one though: raising its level of abstraction, by hiding lower-level details related to resources life cycle. The problem here is that those “lower-level” details are in fact at the same level of abstraction as the code using those smart pointers. Therefore the alias is too high in terms of levels of abstraction.
Another way to see it is that, in general, making an alias allows to some degree to change the type it refers to without going over all of its usages and changing them (a bit like auto
does). But as we’ve seen in this article, changing the type of pointer, from raw pointer to std::unique_ptr
or from std::unique_ptr
to std::shared_ptr
for example, changes the semantics of the pointers and requires to modify many of their usages anyway.
What is your opinion on this? Are you in favor or against aliasing pointer types? Why?
You will also like
- It all comes down to respecting levels of abstraction
- Pointers, References and Optional References in C++
- Smart developers use smart pointers
Share this post!