Why Optional References Didn’t Make It In C++17

An object of type optional<T> can take every value that T can take, plus one. This extra value represents an object that is “null” (or “empty” or “not set”, formulate it as you will). And we’ve already seen how optionals can make your interfaces clearer.

The aspect I’d like to dig in deeper today is the particular case where T is a reference type, such as int& or const Employee& . This makes “optional references”.

We’ve touched upon this topic in the article about pointers, references and optional references to describe the semantics of optional references, and I encourage you to go check it out in case you don’t feel 100% comfortable about them.

In a word, optional references share some semantics with pointers: they can point to something like a normal reference, and they can also point to nothing when they’re a null optional. But they only represent handles and don’t do pointer arithmetics and such low-level features.

But optional references have been quite a controversial topic. So much so that while optional was accepted in C++17, optional references didn’t make it in the standard. So optional references are illegal in C++17.

The debate on optional references revolved around the topic of rebinding. Should the underlying reference rebind on the operator= of the optional?

To understand this, I reached out to Fernando Cacciola, the author of the Boost Optional library (optionals started in Boost long before they got into the standard), and I asked him what the debate was about exactly. Fernando kindly walked me through the rationale for the pros and the cons of optional references, expressed his opinion and allowed me share all this with you today.

Why should you care, you may wonder? After all, optional references didn’t make it in the standard. Actually, there are at least 3 reasons you could benefit from understanding that:

you will get a deeper understanding of optional , which is a very useful component in modern C++,

, which is a very useful component in modern C++, you will get a better understanding of references in C++,

you will see the sort of design trade-offs a very well crafted library must face.

And I’m not even counting that it’s an interesting debate.

To rebind or not to rebind?

Imagine, just for a moment, that optional references were legal in C++ (in fact they are legal in Boost – more on this in a moment), and consider the following code:

int x = 42; std::optional<int&> orx = x; 1 2 int x = 42 ; std :: optional < int & > orx = x ;

orx is an optional reference. It means that it either represents a reference (bound to another object, like all references), or an empty optional (so bound to nothing). In this particular case, the optional is initialized with something, x , which has the effect of initializing the underlying reference by binding it to x .

All good. Now consider these additional lines of code:

int y = 43; orx = y; 1 2 int y = 43 ; orx = y ;

What does the last line mean? Does it do a rebinding, that is to say that the underlying reference of orx is now bound to y ? Or does it forward the assignment the the underlying reference, making x equal to 43?

This was the heart of the debate. Before reading on, I suggest that take a moment to think and make your own opinion this.

Done?

Now lets’s examine the pros and cons of each option. After that you can decide if you keep or change your opinion.

The pros of rebinding

What are the advantages of rebinding? Imagine that in the following code:

int x = 42; std::optional<int&> orx = x; int y = 43; orx = y; 1 2 3 4 5 int x = 42 ; std :: optional < int & > orx = x ; int y = 43 ; orx = y ;

… x remains equal to 42 and the underlying reference of orx is now bound to y .

The first advantage is that this brings consistency between empty optionals and non-empty optionals. Indeed consider this slightly different code:

std::optional<int&> orx; // empty optional, bound to nothing int y = 43; orx = y; 1 2 3 4 std :: optional < int & > orx ; // empty optional, bound to nothing int y = 43 ; orx = y ;

When orx is empty, as in this piece of code, it doesn’t make sense to forward the assignment to the underling reference, since there is no underlying reference – it’s an empty optional. The only thing to do with this empty orx is to bind its underlying reference to y .

So rebinding a non-empty optional reference would make the behaviour of operator= consistent with empty optional references, that have to do. rebinding.

The second advantage of rebinding optional references on operator= is a consequence of the first one. Rebinding brings consistency between optional references and optionals in general. Indeed, if T is not a reference type, the effect of operator= of optional<T> does not depend on emptiness. Having a consistent behaviour between empty and non-empty for optional<T&> would make it consistent with optional<T> in this regard.

Finally, the third advantage of rebinding is that it would still leave the possibility to the user to forward the assignment to the reference anyway. Indeed, the following syntax does just this:

int x = 42; std::optional<int&> orx = x; int y = 43; *orx = y; // now x equals 43 1 2 3 4 5 int x = 42 ; std :: optional < int & > orx = x ; int y = 43 ; * orx = y ; // now x equals 43

So making operator= rebind doesn’t prevent the copy-through anyway.

The cons of rebinding

The main argument for forwarding the assignment to the reference on operator= instead of rebinding is… consistency between optional<T> and optional<T&> . But that was an argument for rebinding, wasn’t it?

It was, but there is another way to see this consistency and one that would go in favour of the copy-through. Indeed, the operator= of optional<T> forwards the assignment to its underlying T . Transposing this for optional<T&> would mean forwarding the assignment to the underlying reference. And assigning to a reference changes the value of the object it binds to, and doesn’t rebind it.

A difficult dilemma

What to choose then? To rebind, or not to rebind? Fernando is all in for rebinding, and this is what optional references do in Boost. But this choice didn’t convince all the members of the C++ standard committee, and the result is that optional references are out of C++17 since they didn’t reach a consensus on the subject of rebinding. This is important to know if you use Boost Optional: be careful when using optional references, they will be hard to migrate to C++17’s std::optional .

As a final note, it’s interesting to see that the aspect of consistency is underlying pretty much all the arguments in this discussion. Regardless of the ultimate outcome of optional references, I think it’s a good lesson to learn: consistency should be a strong driver of your choices when designing an interface.

Now what’s your opinion? Should optional references rebind, or forward the assignment to the underlying reference?

Related articles:

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