optional references .. Why?


I've mentioned std::optional<T&> various times, but each time people wonder why. An optional reference, what's that good for? Just use a pointer ... does the world really need std::optional<T&>?

After trying to explain it to different people, I figured a full blog post would be fair.

If you want every detail about this topic of optional references, please read the original paper. But if you do not want to read that 31 page paper, I'll provide a quick introduction behind the motivation for optional<T&> in this article.

What's wrong with a pointer?

Consider this simple example

struct HttpClient
{
    HttpClient(std::string url, Logger* logger);
    // ...
};

Logger logger;

HttpClient client{"https://example.com", &logger};
// or
HttpClient client{"https://example.com", nullptr}; // perform no logging

HttpClient takes a Logger by pointer. Since it's a pointer, it can point to an object or to null. If you want it to log, give it a logger, otherwise give it nullptr.

But are we correct? Perhaps ownership to a Logger is supposed to be passed into the HttpClient object. The raw pointer doesn't really tell us much about the ownership model. That is where a raw pointer can be tricky and confusing.

A raw pointer doesn't express ownership, lifetime, or tells us whether it points at one object or the first element of an array. Every one of those questions has to be answered by a comment, a naming convention, documentation, or some other form of context.

The reality is that we don't really want to use raw pointers in application code. Ideally we create abstractions, and luckily the C++ Standard Library provides us with many:

- std::unique_ptr and std::shared_ptr own dynamically allocated memory, with different ownership models.
- std::string owns a character sequence.
- std::string_view refers to a character sequence it does not own.
- std::span refers to a contiguous sequence it does not own; std::mdspan does the same for multiple dimensions.
- std::vector owns a contiguous sequence, and so on through the containers.

Every one of these hides a raw pointer. The point isn't that raw pointers are bad. The problem is that the pointer just doesn't tell us enough of how to use it. These abstractions do.

Who owns the logger? Does HttpClient take ownership and delete it later? Is nullptr allowed, or is this one of those C-style APIs where passing null is not allowed? Is it a pointer to one Logger, or to an array of them? In this simple example you can guess and may be right, but guessing won't bring you far in large scale applications.

Meet std::optional<T&>

struct HttpClient
{
    HttpClient(std::string url, std::optional logger);
    // ...
};

Logger logger;

HttpClient a{"https://example.com", logger};
HttpClient b{"https://example.com", std::nullopt}; // no logging

All previous questions are answered by the type:

It's a reference, so the logger is owned somewhere else. It's optional, so a null value is valid It refers to a single Logger, not to an array. There's no pointer arithmetic to worry about.

A second example:

User* findUser(std::string_view name);

// or

std::optional findUser(std::string_view name);

The first returns a pointer. From context you might guess it's a non-owning User* that's null when there's no match. But you don't know for sure. The second version removes any guessing.

The optional version also composes. Because it's still an optional, the monadic operations work:

auto city = findUser("ada")
    .transform([](User& u) -> Address& { return u.address(); })
    .transform([](Address& a) { return a.city(); })
    .value_or("unknown");

What about performance?

As you might expect, std::optional<T&> just wraps a pointer. There's no separate state, and performance wise there's nothing to worry about.

It was accepted for C++26, and is already implemented by GCC and Clang, ready for you to use.

Well, that concludes this introduction to optional references. I've only scratches the surface, but there's much more to read. Check out to the original paper.

Consider subscribing to my e-mail list below: