Pointers, References and Optional References in C++

In C++, one can manipulate objects directly or via something else, which is commonly called a handle. At the beginning of C++, handles could be pointers, references and iterators. Modern C++ brought in reference wrappers, and boost introduced optional references.

The fact that a given piece of code chooses to use one particular handle expresses something. For this reason it is important to know the meaning of each handle, so that you can leverage on them while you read and write code.

Before getting into the specificities of each type of handle, let’s make a brief…

Point about naming

Here is the naming guideline that I recommend for handles:

The name of a handle must be what the name of the object would have been if we held it directly. For example, pointers names shouldn’t start with “ p “, and iterators names shouldn’t start with “ it “.

Indeed there is no need to clutter a name with such extra information: it’s already in its type, if we really want to know. And when reading code, we mostly don’t want to know anyway.

Following this guideline is in fact the natural thing to do. Handles are just thingies that help manipulate another object. Very much like the handle of a piece of luggage in fact. To illustrate, consider the following two versions of the same story:

Version 1:

Developer: “Shall we leave for the airport now?”

Spouse: “Sure, let’s go!”

Developer: “Ok, let me just grab my suitcase and I’m ready to go!”

Version 2:

Developer: “Shall we leave for the airport now?”

Spouse: “Sure, let’s go!”

Developer: “Ok, let me just grab the handle of my suitcase and I’m ready to go!”

Spouse: “You’re so weird.”

Even though it is true and accurate that a suitcase is manipulated with a handle, you don’t want this detail to show in its denomination. The same goes for code.

The good ol’ pointers and references

I learned a significant part of this section from the opening item of Scott Meyer’s More Effective C++.

Nullability

A pointer can point to nothing. A reference can’t (*).

A way to express a pointer pointing to nothing before C++11 is to make it equal to zero:

T* pointer = 0; 1 T * pointer = 0 ;

C++11 introduces nullptr , making it more explicit:

T* pointer = nullptr; 1 T * pointer = nullptr ;

This also helps static analysers better understand the code.

(*) A reference can, technically, be null:

T* pointer = nullptr; T& reference = *pointer; 1 2 T * pointer = nullptr ; T & reference = * pointer ;

This seems dumb, but if the reference and the pointer are several stack layers away from each other, it’s harder to spot. Anyway, the convention for references is that they should never be null.

Rebinding

We can make a pointer point to something different in the course of its life. A reference points to the same object during all its lifetime.

To rebind a pointer:

T object1; T object2; T* pointer = &object1; // pointer points to object1 pointer = &object2; // pointer points to object2 1 2 3 4 5 T object1 ; T object2 ; T * pointer = &object1 ; // pointer points to object1 pointer = &object2 ; // pointer points to object2

The same syntax transposed to references makes an assignment on object1 :

T object1; T object2; T& reference = object1; // reference points to object1 reference = object2; // equivalent to: object1 = object2 1 2 3 4 5 T object1 ; T object2 ; T & reference = object1 ; // reference points to object1 reference = object2 ; // equivalent to: object1 = object2

Should I use a pointer or a reference?

Pointers are more powerful than references in the sense that they allow two things that references don’t: nullability and rebinding. And as you know, a great power comes with great responsibilities: you need to worry about a pointer not being null, and to follow its life to check for target changes.

For this reason, unless you need the additional functionalities of pointers, you should use references.

Other differences

Pointers and references have a different syntax: pointers access the pointed object with * or -> , and references have the same syntax as direct access to the object.

Finally, a failed dynamic_cast doesn’t have the same effect on a pointer and reference:

a failed dynamic_cast on a pointer returns a null pointer ,

on a pointer returns a , a failed dynamic_cast on a reference throws an exception of type std::bad_cast . Which makes sense because it can’t return a null reference.

std::reference_wrapper

The fact that references can’t rebind makes them unfriendly with operator= . Consider the following class:

class MyClass { public: MyClass& operator=(MyClass const& other) { ??? } // ... private: T& reference; }; 1 2 3 4 5 6 7 8 9 10 11 class MyClass { public : MyClass & operator = ( MyClass const & other ) { ? ? ? } // ... private : T & reference ; } ;

What should operator= do? The natural thing would be to make reference point to the same object as other.reference does, but references can’t rebind. For this reason, the compiler gives up and doesn’t implement a default assignment operator in this case.

std::reference_wrapper , from the <functional> header, provides a way out of this, by wrapping a reference into a assignable (and copyable) object. It comes with the std::ref helper to avoid typing template parameters:

T object1; auto reference = std::ref(object1); // reference is of type std::reference_wrapper<T> 1 2 T object1 ; auto reference = std :: ref ( object1 ) ; // reference is of type std::reference_wrapper<T>

Its operator= does the natural thing, rebinding:

T object1; auto reference = std::ref(object1); // reference points to object1 T object2; reference = std::ref(object2); // reference now points to object2 // object 1 hasn't changed 1 2 3 4 5 T object1 ; auto reference = std :: ref ( object1 ) ; // reference points to object1 T object2 ; reference = std :: ref ( object2 ) ; // reference now points to object2 // object 1 hasn't changed

Replacing T& with std::reference_wrapper<T> in MyClass solves the problem of operator= , because the compiler can then implement it by just calling operator= on the std::reference_wrapper<T> .

Note that we can assume that std::refrence_wrapper always points to something, since it wraps a reference that is supposed to point to something.

If you wonder how it works, std::reference_wrapper can be implemented with a pointer to the object pointed by the reference it is passed.

Optional References

Optional objects were first introduced in boost. An optional<T> represents an object of type T, but that can be “null”, “empty” or “not set” as you will.

In the case where T is a reference boost::optional<T> has interesting semantics:

when the optional is not null it points to something , like a normal reference,

, like a normal reference, it can point to nothing , by being a null optional (an optional can be nullopt )

, by being a null optional (an optional can be ) it can rebind through its operator= , like std::reference_wrapper .

And this looks exactly like… the features of a pointer!

What differentiates the very modern-looking optional reference from our old-fashioned pointer then?

The answer is the low-level aspects of pointers. Like pointer arithmetics, array semantics, and the fact that a pointer can be used to model a memory address.

For this reason, optional references better model a handle than a pointer does.

However, since the C++ standard committee members weren’t all convinced that assignment on optional references should do rebinding, optional references didn’t make it into C++17. Maybe the committee will reconsider them for a future version of the language though.

A practical consequence of this is that if you’re using boost optional references now, your code won’t integrate seamlessly with std::optional when you upgrade to C++17. This constitutes a drawback to optional references, even if it isn’t coming from an intrinsic problem.

Pack up and go

In summary,

References cannot be null and cannot rebind ,

and , std::reference_wrapper cannot be null but can rebind ,

but , Pointers can be null and can rebind (and can do low-level address manipulations),

and (and can do low-level address manipulations), boost optional references can be null and can rebind (but are incompatible with std::optional ).

As you see, there are multiple handles that can hold a suitcase. You just need to pick the one that fits your need and nothing more, and off you go.

Related articles:

Share this post! Don't want to miss out ?