Logo

Blog


Down with pointers

Some years ago, there was an Aprils fool post from various other C++ bloggers that C++ has deprecated pointers (for example, Fluent C++ - C++ Will No Longer Have Pointers. Well, as C++ nearly never deprecates anything, that alone was unbelievable. However, do we still need pointers? That's the question I want to cover in today's post.

What pointers say

In C++, pointers signal that a parameter may have a value, or not. Whenever a function receives a pointer, we should write a check in the body whether the parameter is a nullptr. Sadly, I already saw many cases where this check was omitted. All documentation and comments like a valid non-null object is required do not help and don't make that check obsolete.

I also have seen cases where the nullptr check on a function parameter was omitted because it was hard to decide what to do in the circumstance of a nullptr. Say a function that returns void but has received a nullptr.

The other interesting part is that this check comes with costs. The compiler, at least from what I have seen, is not able to optimize such a check away, even in a small program. See below for more details.

Use references instead of pointers

This necessity for a check, and the endless comments, go away once we switch to a reference. In contrast to a pointer, a reference expresses that a valid object is required at this point.

A simple approach is to still receive pointers on API boundaries if, for example, you can't change the API. But then, first thing in that function, do the nullptr-check, return if the pointer is null. If it is valid, dereference the pointer and store it in a reference.

1
2
3
4
5
6
7
8
bool DoSomeStuff(int* data)
{
  if(nullptr == data) { return false; }

  int& refData = *data;

  return HandleData(refData);
}

That way, we can at least keep the internal API and code clean. Maybe with the next release, we will get a chance to clean up the public API as well.

Wait, I need a maybe parameter

Okay, then let's change all pointers to references. But what if I need such a maybe parameter? Hm, with maybe you mean optional? Right! For that case, C++17 brings us std::optional. So please stop abusing pointers when you want to express that the parameter is optional. No need to convert an int into an int* just to have the value nullptr available for comparison.

1
2
3
4
5
6
bool DoSomeStuff(std::optional<int> data)
{
  if(data.has_value()) { return HandleData(data.value()); }

  return false;
}

The datatype std::optional is so much better than a pointer. With functions like get_value_or, it spares us writing an annoying if which adjusts the value to the stored one or the default.

Okay, but what is with, say, an array? Say we want to pass an array to a function there, we can't use references, except if we make it a template. Oh, and please don't say std::array because I want this function to be callable with various different array sizes. There I still need a pointer! Got you!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void IWantPointers(const char* data, const size_t length)
{
  for(int i = 0; i < length; ++i) { std::cout << data[i]; }
}

void Use()
{
  char data[]{"Hello, Pointers\n"};

  IWantPointers(data, sizeof(data));
}

span and string_view to the rescue

Well, no. At least we don't need a pointer in the API of the function. C++20 brings us std::span for cases where we want to pass an array or a contiguous container (in this example here, we might also use std::string_view from C++17). The advantage of std::span is that it carries the number of elements of the data. So no additional size parameter and way fewer sizeof's.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void IWantPointers(std::span<const char> data)
{
  for(const auto& c : data) { std::cout << c; }
}

void Use()
{
  char data[]{"Hello, Pointers\n"};

  IWantPointers(data);
}

I think we are at a stage where we can say that there is no need for a pointer for a top-level API anymore. With helper types like std::optional and std::span, we can do much better. And yes, pointers are still a thing in C++ and should be. For example, std::span takes and returns a pointer.

Why do I care that much?

Well, I like clean and expressive APIs. What I also like is efficient code. Have a look at the following example on Compiler Explorer and see for yourself godbolt.org/z/T6qq5q3Tb. You see a full program, including main. The function Fun that takes a pointer and checks for nullptr consumes 7 instructions with -O3. The version without a check, as well as the reference version, only consumes 3 instructions. This is for a case where the compiler sees the entire program! The interesting part is Opt. Here I use a std::optional together with get_value_or. So essentially, the value is checked. However, both Clang and GCC manage to compile that function to 6 lines of assembly. Not bad, right? Okay, the library part is missing here, so we get some additional costs for the optional itself.

Do we still need pointers?

Well, I hope I showed you that we at least need them less frequently than we used to. Pointers are still an essential part of C++, but we can use better data types in a lot of places.

Andreas