If you’re a regular reader of my blog, you know I’ve been sharing what I learn about new C++ language and library features ever since C++20. You probably also read my CppCon 2025 Trip Report. And this post is where the two come together.
At CppCon I attended a great talk by Steve Downey about std::optional<T&>. Steve is the father of optional references—he co-authored P2988R12 with Peter Sommerlad.
Let’s start with a little history. By now — at the end of 2025 — even C++17 feels like history. That’s when std::optional<T> was introduced, giving us a way to represent “maybe a value” with value semantics, instead of relying on pointers. But std::optional in C++17 (and later) couldn’t hold references — unless you wrapped them in std::reference_wrapper.
C++26 finally fixes this gap.
What is std::optional<T&>?
std::optional<T&> has three key characteristics:
- Unlike std::optional<T>, it is not an owning type. It simply refers to an existing object.
- It provides both reference and value-like semantics.
- Internally, it behaves like a pointer to T, which may also be nullptr.
This last point is interesting: while optional<T> can be seen as variant<T, monostate>, optional<T&> is closer to variant<T&, nullptr_t>. In practice, it’s a safer alternative to a non-owning raw pointer.
Does this mean one less reason to use raw pointers?
Well, owning raw pointers have been discouraged since C++11. Non-owning raw pointers are still common for expressing “I don’t own this.” With C++26, optional<T&> might replace many of those cases. You’ll still need to check engagement, but at least it’s more expressive than sprinkling nullptr checks everywhere.
Key considerations
The design of std::optional<T&> aimed for the least surprising and least dangerous behavior. Let’s walk through some of the decisions.
Assign or Rebind?
Consider this snippet (from Steve Downey’s CppCon talk):
1 2 3 4 5 6 | Cat fynn; Cat loki; std::optional<Cat&> maybeCat1; std::optional<Cat&> maybeCat2{fynn}; maybeCat1 = fynn; maybeCat2 = loki; |
What should these assignments do? Should they copy objects or rebind references?
If they were true assignments, maybeCat2 = loki; would copy loki into fynn. That would have been surprising and error-prone.
Instead, the committee decided that operator= for optional<T&> always rebinds the reference. At the end of the snippet:
- maybeCat1 still refers to fynn (the = fynn was redundant).
- maybeCat2 now refers to loki, not fynn.
This design makes the behavior consistent and avoids accidental copies.
What about make_optional()?
make_optional() returns an optional<T>, not optional<T&>. Even if you pass a reference, it still creates an owning optional. This is intentional: allowing make_optional<T&> would lead to dangling references.
In practice, make_optional<T&> was mostly used in test cases. With C++26, you’ll need to construct optional<T&> directly if you want an optional reference.
It’s also worth noting that the main usage of make_optional<T&> are tests cases.
The question of constness
Should optional<T&> model shallow or deep const?
For a const optional<T&>, should operator*() and operator->() yield T& or const T&?
The designers chose shallow constness: dereferencing a const optional<T&> still gives you a non-const T&. If you need deep constness, you can use optional<const T&>.
The least dangerous value_or
What about value_or?
For optional<T>, it returns a T. For optional<T&>, the safest design turned out to be the same: value_or returns a T (by value).
This avoids surprising reference semantics, and it supports common use cases like providing a literal fallback:
1 | auto name = maybeName.value_or("Anonymous"s); |
Steve also mentions in the paper future proposals for free functions like reference_or, value_or, or_invoke, and yield_if for all optional-like types. Stay tuned — I’ll probably cover those here soon.
Conclusion
std::optional<T&> fills an important gap in the language. It gives us a safer and more expressive way to model maybe-a-reference, reducing our reliance on raw pointers and reference wrappers.
It’s another step toward making code both more readable and less error-prone — two things I’m always happy to see in modern C++.
Connect deeper
If you liked this article, please
- hit on the like button,
- subscribe to my newsletter
- and let’s connect on Twitter!