Tip of the Week #163: Passing std::optional parameters

Originally posted as TotW #163 on July 11, 2019

By Ian Eldred Pudney

Updated 2020-04-06

Quicklink: abseil.io/tips/163

Are nulls really a billion-dollar mistake?

The problem

Let’s say you need to implement a function with a parameter that may or may not exist. You might be tempted to use modern, fancy-schmancy std::optional for this. However, if the object is big enough that it should be passed by reference, std::optional is probably not what you want. Consider the following two declarations:

void MyFunc(const std::optional<Foo>& foo);  // May copy by value
void MyFunc(std::optional<const Foo&> foo);  // Doesn't compile

The first option probably doesn’t do what you want. If someone passes a std::optional<Foo> into MyFunc, it is passed by reference, but if someone passes a Foo into MyFunc (for example as a return value), the Foo will be copied by value into a temporary std::optional<Foo>, which will then be passed by reference into the function. If your goal was to avoid copying the Foo, you haven’t.

The second option would be great, but unfortunately is not supported by std::optional.

Recommendation

Avoid function parameters of the form const std::optional&.

If your object is small enough to not need pass-by-reference, you should take the object wrapped in an std::optional by value. For example:

void MyFunc(std::optional<int> bar);
void MyFunc(std::optional<absl::string_view> baz);

If you are intentionally making a copy of the argument, you should also accept the std::optional by value to make that clear:

void MyFunc(std::optional<Foo> foo);

Otherwise, skip the std::optional altogether.

You can pass it by absl::Nullable<const Foo*> and let nullptr indicate “does not exist.”

void MyFunc(absl::Nullable<const Foo*> foo);

This will be just as efficient as passing by const Foo&, but supports null values. See Tip #116 for more on when to use a pointer instead of a const reference.

Then what on Earth is std::optional for???

std::optional can be used if you own the thing that’s optional. For example, class members and function return values often work well with std::optional.

Exception

If you expect all callers of your function to already have a std::optional<Foo> and never pass in a Foo, then you may take a const std::optional<Foo>&. However, this is rare; it usually only occurs if your function is private within your own file/library.

What about std::reference_wrapper?

The documentation for std::optional points out that you can use a std::reference_wrapper to work around the fact that optional references aren’t supported:

void MyFunc(std::optional<std::reference_wrapper<const Foo>> foo);

However, we don’t recommend this:

  • std::reference_wrapper has surprisingly subtle semantics, making it difficult to understand and use safely. For instance, various utilities in the standard library special case it in ways that make it act differently from a normal value or reference.
  • std::optional<std::reference_wrapper<const Foo>> is cumbersome and verbose, compared to absl::Nullable<const Foo*>.

Subscribe to the Abseil Blog