Using optional_view to manage optional references in C++

Igor Machado
4 min readJun 22, 2023

--

Reference type is a complicated subject in C++, although necessary to program certain behaviors. A classic application is on assignment operators, where a reference to the type itself is returned. In many other scenarios, a reference T& can be replaced by a pointer T*, what brings other issues, due to the lack of proper semantics behind a raw pointer and the fact that it is nullable (and a reference is not, by definition).

Optional type optional<T> have helped improve the language in standard C++17 with clear ownership rules, allowing stack-based elements to be efficiently allocated, also providing a flag std::nullopt to indicate a lack of such elements. However, optional references, such as optional<T&> did not make into the standard due to many debates over rebind behavior in reference<T&>. The general question asked is: should an optional reference rebind after assignment, or should assignment be performed on its internal element? As indicated on Herb Sutter’s “References, Simply” post, “An astonishing amount of ink has been spilled on this particular question for years, and it’s not slowing down”, so let’s not spend more time on optional<T&> interface here. It’s time to discuss a better interface with clear semantics: optional_view<T>.

The Problem

Consider some large structure named Big, and a function whats to receive it as parameter to get some data from it. A common strategy is to use a reference, preferably, a const reference:

void func(const Big& b) { ... }

However, there may be situations where one cannot guarantee the existence of such parameter, requiring it to be optional. A classic solution is to use a pointer:

void func(Big* b) { ... }

This can represent the desired behavior, but in a poor and limited manner… what ownership semantics apply to the parameter? So, another solution is to use an optional<T>:

void func(std::optional<Big> b) { ... }

This way, one may efficiently pass the variable using move semantics, but this also forces the user to have it packed as optional before:

void invoke_func() { 
Big b;
func(std::make_optional<Big>(b));
}

What happens if user also has a variable Big hosted in heap, such as in std::unique_ptr<Big>? Now, we cannot simply pass it by reference to a simple read. We also don’t have std::optional<T&>. What we actually need is a view on the data, such as the recently proposed string_view type, that efficiently replaces const string& .

The Proposed Solution

So, we propose a new type named optional_view<T>, that works as a reference T&, guaranteeing immutability (thus, no rebind), and also nullability: an optional_view can be in disengaged state. In this sense, the semantics of a view means pretty much the same as in string_view: non-ownership and efficiency on copy, and no assignment operator. Basically, an optional_view<T> works as a T*, but with the clear semantics of a view. Since we don’t need an assignment operator, the ambiguity of optional<T&> is automatically solved!

So, advantages are: (i) Clear ownership and lifetime management; (ii) No rebinding (same as T&); (iii) immutable behavior: once bound to reference in constructor, it will never unbind, and if disengaged, will remain disengaged; (iv) allows nullability for a type equivalent to T&; (v) compatibility with other STL containers, such as std::optional, std::unique_ptr, and also classic T&.

Due to the lack of move semantics, a limitation is that optional_view it is not capable of performing Lifetime Extension, such as const X&. We believe that that it is possible to implement that as well, but managing ownership (as in temporary lifetime extension) and non-ownership together may bring some doubts, so it’s better to have different naming for that extensions. In this sense, an optional_unique_view can represent an unique and optional view, with move semantics that is capable of managing lifetime extension (through the internal usage of std::unique_ptr<T> instead of T*), but copy capabilities are lost; and an optional_shared_view can also deal with lifetime extension, also keeping both copy and move semantics (with the only classic limitations known for std::shared_ptr).

An Example

An implementation of optional_view can be found on GitHub at https://github.com/igormcoelho/optional_view. In the example below, we explore some different situations of parameter passing on a function that expects an int& parameter called maybe_int that can be nullable: thus, an optional_view<int>.

#include <iostream>
#include <memory>
#include <opview/optional_view.hpp>

using opview::optional_view;

void f(optional_view<int> maybe_int) {
if (maybe_int)
std::cout << *maybe_int << std::endl;
else
std::cout << "empty" << std::endl;
}

int main() {
int x = 10;
f(x); // prints 10
//
optional_view<int> ox{x};
f(ox); // prints 10
f(std::nullopt); // prints "empty"
// f(10); // ERROR: no move semantics (non-ownership)
auto z = std::make_unique<int>(5);
f(*z); // OK: prints 5
//
std::optional<int> op_y{20}; // OK for std::optional...
f(op_y); // compatible: prints 20
//
x = 40; // changes x from 10 to 40
f(ox); // prints 40 (view behavior from x...)
*ox = 50; // allows mutable data change for int
f(ox); // prints 50
//
std::cout << x << std::endl; // prints 50
//
std::cout << *op_y << std::endl; // prints 20
optional_view<const int> oz{op_y};
// f(oz); // ERROR: cannot const_cast from const int to int
std::cout << *oz << std::endl; // prints 20
// *oz = 30; // disallows data change for const int
//
*op_y = 25; // remote change on std::optional
std::cout << *oz << std::endl; // prints 25
//
// optional_view<int> ow{oz}; // ERROR: ‘const int’ to ‘int&’

return 0;
}

Final words

We believe that optional_view is a better and cleaner approach to the problems faced by optional references, also providing greater efficiency and compatibility with other STL containers. Please try that and leave some comments on GitHub. Ideas and suggestions are very welcome!

Disclaimer: I’m the author of optional_view C++ library.

--

--