Jonathan Müller recently wrote a blog post arguing (among other things) that we don’t need
optional<T&>, making the points that:
- For function parameters, overloading works better
- For returning an object that may or may not exist, return a
I would like here to argue against those points — that overloading is worse than taking an
optional and returning an
optional<T&> is strictly better than returning a
T*. I’ll then conclude with another argument in favor of why we do need
(To be fair, there are many things in the post that I agree with, and everyone should read it, but the big takeaway is very much that we don’t need
optional<T&> and I strongly disagree).
Function parameters. Let’s take the simplest case where we just want a function that takes a defaulted argument. There are three ways we could do that:
Some of these things are simply matters of style, but I just don’t like the overloading approach. We’re writing multiple things (in this case, overloads) to express one idea (a defaulted argument). That’s not a clean way to express it. Moreover, what if we had two defaulted parameters? I’d need three overloads now?
Default arguments are tricky and have some caveats (see Michael Price’s 2017 CppCon talk), but many of those occur when you either do weird things (like have multiple declarations of a function that default different arguments, or just generally have multiple declarations) or have potentially expensive effects that are invisible — and neither of those really apply to the
optional case since the default argument is just
nullopt). It’s simpler, more direct, and more concise.
But what happens when we want to have another function call our
foo and pass-through the default argument? How do we do it?
If we wrote two overloads, we’d have to write two overloads… everywhere. More duplicated work. Or we could write a variadic function template? Which is an enormous hammer that seems out of place here — especially since we really only want 0 or 1 argument. Don’t get me wrong, I’m all for using templates to do cool things. This just doesn’t seem like a good solution.
If we used a default argument, we have to know what that default argument was and repeat it. This works — but what if
foo2 wants to change its default for a good reason? It’s very easy to get out of sync, and then, how do you know if
bar2 have different defaults on purpose or by accident?
bar3, we don’t have to worry about any of this. Just one overload to solve one problem, and the only place that has to worry about the default is within the body of
foo3, where it belongs.
Furthermore, not all uses of
optional function parameters are there to handle defaults. In some cases, parameters are truly optional. In that case, how do you even address this use case with overloads? I can’t think of a solution that isn’t to just take an
optional<T>. It just works.
Returning a value. Perhaps the canonical example for returning an optional value is the case of map lookup. Let’s compare our choices, between returning an
optional<T&> and, as Jonathan suggests, returning a
Here’s the thing. Take an unknown function that just returns a pointer to object. What can its value be? It could be…
- A pointer to an object that the caller does not own
- A pointer to an object that the caller owns
- A pointer to an array of objects that the caller does not own
- A pointer to an array of objects that the caller does own
- A pointer within a range, or one past the end of a range
- A pointer to uninitialized memory suitable to hold an object, for the caller to construct into
That’s a lot of different semantics wrapped in the simple type
T*. Now, there might be some quibbling about how in modern C++, raw pointers are non-owning and smart pointers are owning. But there is still so much old code exists that does not make this distinction, not to mention the entire language of C, so I don’t think we can so cavalierly reject owning raw pointers.
optional<T&> can only ever be the first two: null, or a pointer to an object that the caller does not own. That means that when you see
optional<T&> f(U), you immediately know what this function means, and how to use its return, in a way that you cannot know for
T* f(U) until you read the documentation.
T*'s many semantics, it has many operations which compile and do reasonable things in some cases, but are total nonsense in others. In the case where we would consider
optional<T&> as an alternative, those are:
- Incrementing/decrementing/adding or subtracting an integer
- Subtracting a pointer
None of those compile for
optional<T&>, which is fantastic. You cannot misuse it, unless you write something explicit that looks ridiculous on its face.
To me, all of that is a big win in
optional<T&>'s favor. It’s easier to understand, and harder to misuse.
Lastly, a quick argument as to why we need optional references: generic code. Having a hole for your type makes it hard to use in generic code, and suddenly everyone has to figure out how to work around it. P0798 is proposing to add monadic operations to
optional, which brings this issue to the forefront.
What should the type of
n be? It could be:
- ill-formed, continue to not support optional references, don’t add a workaround
optional<string&>, that’s what
optional<string>, if we decayed due to the lack of optional references
optional<reference_wrapper<string>>, if we had the library work around this for us
I’d argue (2) is the expected, most reasonable outcome of this operation. (1) is pretty user-hostile. (3) has to make a copy of the string, in a context which doesn’t look like it requires it. That’d be pretty surprising to say the least. (4) is functional and efficient, and I guess it’s nice that the standard library would do this wrapping for us. But the standard library isn’t the only piece of code that might need to do something like this. Everyone else would, everywhere. For what benefit?
map() makes for an easy, short example. But just generalize the idea out to any generic context that might need to produce an
optional. It’s so much easier, for everyone, to just make
In short, I agree with Jonathan on many things. An
optional<T&> is not a
T* — it’s much better at the things that you could use an
optional<T&> for, and totally useless at everything else. Assignment should rebind.
But do we need