Contrary to Sunday’s orchestrated April Fool’s posts, raw pointers are not going anywhere. However, there are some things in those posts that are based on reality. I’ll go into a few of them today.
The role of raw pointers
Raw pointers are an essential part of the Language and one of its low-level, basic building blocks. They are used in lots of places, e.g. in the implementation of standard library containers and their iterators, smart pointers and other elemental pieces of functionality.
For user defined classes, the use of raw pointers should ideally also be restricted to low-level implementation details. We usually want to build our higher level logic on layers of lower level abstractions. The latter still provide some utility and safety over the basic building blocks of the language like raw pointers.
In this post I will point out some of the abstractions we should use where raw pointers are often used in many code bases.
You have probably heard this before: “
delete are considered code smells”. That means, unless they appear in classes like containers and smart pointers that are explicitly dedicated to memory management. The same applies for
free, of course. With that, manual memory management via raw pointers is a no-go.
If you happen to come across a use case where
std::shared_ptr are not the right abstraction, write your own class for the specific way of memory management you need. This has several benefits: First of all you make this way of memory management reusable. Beyond that, it separates the juggling of raw pointers and memory allocations from the rest of your program’s logic. Lastly, it is easier to modify later, as all of the memory management logic is in one place.
It may seem complicated to condense memory management logic into its own class – or family of classes, as in
shared_ptr. But if it’s hard to get it right in a single confined spot, it’s unlikely that we get it right when it is distributed throughout our whole code base. In addition, when we go through the effort of condensing it into its own class, it is much easier to add a thorough suite of unit tests to make sure we did not miss an edge case.
The second major use of raw pointers we see is iteration over contiguous memory, a.k.a. arrays. For stack-based arrays we have
std::array, which we should prefer over raw C array. And of course we have good old
std::vector to manage our dynamically allocated arrays. Both have iterators that are zero-cost abstractions over raw pointers (or usually close enough) in Release builds.
In Debug builds, however, most standard library implementations provide checked iterators that help us find out-of-range errors where raw pointers would simply lead to undefined behavior a.k.a. crashes – if we’re lucky.
What about when we have to access C-arrays in libraries or similar things? The notorious raw pointer plus length pairs are ubiquitous in C-style APIs. For this, we have the
span class in the Guideline Support Library (GSL) which will be in the standard library in C++20. A span is a class that wraps those pointer+length pairs and provides – you guessed it – proper iterators.
But what about pointers that are simply a nullable reference to another object, without any memory ownership semantics and all the buzz? The standards committee has you covered. In the library fundamentals TS v2 we have
observer_ptr that is meant to be a drop-in for raw pointers in the described situation.
Other than raw pointers, it does not have increment and decrement operators, because it’s not an iterator. It also has explicit
release methods, much like the standard smart pointers we know.
For optional reference function parameters, consider to overload your function with a reference parameter and completely without the parameter. It makes the semantics of the reference being optional much clearer than a pointer.
Use static analyzers!
Modern static analyzers help a lot with finding raw pointers that are unsafe to use or that have better alternatives. Especially check for analyzers that check for violations of the C++ Core Guidelines.
For edge cases, e.g. where you have owning raw pointers going into or coming out of a legacy API, the aforementioned GSL provides type aliases like
not_null<T>. Those still are plain raw pointers, but they provide clues to the reader and static analyzer what the intended semantics of the pointer are.
As a default, restrict your raw pointers to the very low levels of your implementations and rely on zero-cost abstractions for code on higher levels.
Did I miss something? Please leave your comment below!